/* ═══════════════════════════════════════════════════════════════════════════
   site.css, the house stylesheet for calebsargeant.com

   Source of truth: docs/design-system.md. Every token name, class name and
   animation below is specified there. If a name here disagrees with that
   document, that document wins and this file is the bug.

   Order: tokens, reset/base, primitives, components (in design-system.md
   section 3 order), animations, responsive, reduced motion, print.

   assets/print.css is a different stylesheet for a different job: it dresses
   templates/print/* for the PDF renderer. The @media print block at the foot
   of this file only has to make the *site* pages printable from a browser.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ── 1. Tokens ───────────────────────────────────────────────────────────── */

:root {
  /* ground */
  --ink:        #080B10;
  --ink-2:      #0D131B;
  --ink-3:      #121A24;
  --line:       rgba(255, 255, 255, 0.075);
  --line-2:     rgba(255, 255, 255, 0.14);

  /* text */
  --fg:         #EAF2F8;
  --fg-dim:     #9FB3C4;
  --fg-mute:    #748A9F;   /* AA on --ink/--ink-2/--ink-3: 5.52 / 5.22 / 4.90 */

  /* accent: "signal" */
  --sig:        #38E1FF;
  --sig-2:      #4C6FFF;
  --sig-ink:    #7FEAFF;
  --sig-wash:   rgba(56, 225, 255, 0.08);
  --grad-sig:   linear-gradient(120deg, var(--sig), var(--sig-2));

  /* status */
  --live:       #4ADE80;
  --warn:       #FBBF24;
  --crit:       #F87171;

  /* type */
  --font-display:    "Space Grotesk", system-ui, -apple-system, sans-serif;
  --font-mono:       "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
  --tracking-kicker:  0.24em;
  --tracking-display: -0.035em;

  /* geometry */
  --maxw:          1180px;
  --radius-card:   20px;
  --radius-panel:  14px;
  --radius-btn:    11px;
  --radius-chip:   7px;

  /* Not part of the published token set: motion and depth. These are theme
     independent on purpose, so they never need a second definition below. */
  --ease:      cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-out:  cubic-bezier(0.16, 1, 0.30, 1);
  --dur:       220ms;
  --dur-slow:  520ms;
  --shadow-1:  0 1px 2px rgba(8, 11, 16, 0.18), 0 10px 30px rgba(8, 11, 16, 0.14);
  --shadow-2:  0 2px 8px rgba(8, 11, 16, 0.24), 0 28px 64px rgba(8, 11, 16, 0.22);

  color-scheme: dark;
}

/* The light theme redefines the listed names and nothing else. Any colour that
   exists only here would be undefined in dark, which is why the block above
   carries every base definition. */
:root[data-theme="light"] {
  --ink:      #F7FAFC;
  --ink-2:    #FFFFFF;
  --ink-3:    #EEF3F8;
  --line:     rgba(8, 11, 16, 0.10);
  --line-2:   rgba(8, 11, 16, 0.18);
  --fg:       #0B1219;
  --fg-dim:   #44586B;
  --fg-mute:  #596E82;     /* AA on --ink/--ink-2/--ink-3: 5.04 / 5.28 / 4.73 */
  --sig:      #0891B2;
  --sig-2:    #3B4FE0;
  --sig-ink:  #0E7490;

  color-scheme: light;
}

/* Honour the OS preference only while the reader has not chosen. An explicit
   data-theme="dark" has to keep winning here, hence the :not() guard. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --ink:      #F7FAFC;
    --ink-2:    #FFFFFF;
    --ink-3:    #EEF3F8;
    --line:     rgba(8, 11, 16, 0.10);
    --line-2:   rgba(8, 11, 16, 0.18);
    --fg:       #0B1219;
    --fg-dim:   #44586B;
    --fg-mute:  #596E82;     /* AA on --ink/--ink-2/--ink-3: 5.04 / 5.28 / 4.73 */
    --sig:      #0891B2;
    --sig-2:    #3B4FE0;
    --sig-ink:  #0E7490;

    color-scheme: light;
  }
}


/* ── 2. Reset and base ───────────────────────────────────────────────────── */

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

* { margin: 0; }

html {
  /* The ambient layer sits at z-index -1, so the page ground has to be painted
     by <html> rather than propagated up from <body>, or the layer disappears
     underneath it. */
  background: var(--ink);
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 96px;   /* sticky nav must not cover an anchor target */
}

body {
  background: transparent;
  color: var(--fg);
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 1.6;
  font-weight: 400;
  min-height: 100svh;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Ambient telemetry ground: two low-alpha signal glows, a 1px grid at roughly
   2% and a vignette that keeps the grid from reaching the corners. Built from
   the accent tokens with color-mix so it re-tints itself in the light theme
   instead of needing a second definition. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(130% 95% at 50% 6%,
      transparent 0%, transparent 52%, var(--ink) 100%),
    linear-gradient(to right,
      color-mix(in srgb, var(--line) 30%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom,
      color-mix(in srgb, var(--line) 30%, transparent) 1px, transparent 1px),
    radial-gradient(46vmax 34vmax at 8% -8%,
      color-mix(in oklab, var(--sig) 15%, transparent) 0%, transparent 68%),
    radial-gradient(42vmax 32vmax at 94% 4%,
      color-mix(in oklab, var(--sig-2) 14%, transparent) 0%, transparent 68%);
  background-size: 100% 100%, 54px 54px, 54px 54px, 100% 100%, 100% 100%;
}

h1, h2, h3, h4, h5, h6 {
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

h1 {
  font-size: clamp(40px, 7vw, 76px);
  line-height: 0.98;
  letter-spacing: var(--tracking-display);
  font-weight: 700;
}

h2 {
  font-size: clamp(26px, 3.4vw, 36px);
  letter-spacing: -0.028em;
}

h3 { font-size: clamp(19px, 1.8vw, 22px); }
h4 { font-size: 16px; }

p { text-wrap: pretty; }

a {
  color: var(--sig-ink);
  text-decoration-color: color-mix(in srgb, var(--sig) 45%, transparent);
  text-underline-offset: 3px;
  transition: color var(--dur) var(--ease),
              text-decoration-color var(--dur) var(--ease);
}

a:hover {
  color: var(--sig);
  text-decoration-color: currentColor;
}

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

ul, ol { padding-left: 1.15em; }
li { margin-block: 0.28em; }
li::marker { color: var(--fg-mute); }

strong, b { font-weight: 600; color: var(--fg); }

hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin-block: 32px;
}

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

code {
  background: var(--ink-3);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 0.1em 0.4em;
  color: var(--sig-ink);
}

pre {
  background: var(--ink-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  padding: 18px 20px;
  overflow-x: auto;
}

pre code { background: none; border: 0; padding: 0; color: inherit; }

kbd {
  font-size: 11px;
  line-height: 1;
  padding: 4px 6px;
  border-radius: 6px;
  color: var(--fg-dim);
  background: var(--ink-3);
  border: 1px solid var(--line-2);
  border-bottom-width: 2px;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 15px;
}

th, td {
  text-align: left;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
}

th {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--fg-dim);
  font-weight: 600;
}

tbody tr:nth-child(even) { background: var(--ink-3); }

blockquote {
  border-left: 2px solid var(--sig);
  padding-left: 18px;
  color: var(--fg-dim);
}

details summary { cursor: pointer; }

/* The accessibility floor: one visible ring, in the accent, on everything that
   takes focus. Outline (not box-shadow) so it follows the border radius and
   survives forced-colors mode. */
:focus-visible {
  outline: 2px solid var(--sig);
  outline-offset: 3px;
}

::selection {
  background: var(--sig);
  color: var(--ink);
}

::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: var(--ink); }
::-webkit-scrollbar-thumb {
  background: var(--line-2);
  border: 3px solid var(--ink);
  border-radius: 99px;
}
::-webkit-scrollbar-thumb:hover { background: var(--fg-mute); }


/* ── 3. Layout primitives ────────────────────────────────────────────────── */

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: 32px;
}

.section { padding: 88px 0; }
.section + .section { padding-top: 0; }

.section-head {
  max-width: 640px;
  margin-bottom: 40px;
}

.section-head h2 { margin-top: 12px; }

.section-head .lead,
.section-head p { margin-top: 14px; }

.section-head.center {
  margin-inline: auto;
  text-align: center;
}

.section-head.center .kicker { justify-content: center; }

.lead {
  font-size: clamp(17px, 1.6vw, 20px);
  line-height: 1.55;
  color: var(--fg-dim);
  max-width: 62ch;
}

.grid-2,
.grid-3 {
  display: grid;
  gap: 22px;
}

.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.panel {
  position: relative;
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  padding: 22px 24px;
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}

.panel:hover { border-color: var(--line-2); }

.card {
  position: relative;
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 26px 26px 28px;
  isolation: isolate;
  transition: transform var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}

/* The hairline brightens and a soft accent glow blooms behind the card. The
   glow has to sit well outside the border box: the card background is opaque,
   so anything inset behind it is simply covered up. */
.card::after {
  content: "";
  position: absolute;
  inset: -26px;
  border-radius: calc(var(--radius-card) + 26px);
  z-index: -1;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(58% 52% at 50% 34%,
    color-mix(in oklab, var(--sig) 30%, transparent) 0%, transparent 72%);
  filter: blur(14px);
  transition: opacity var(--dur-slow) var(--ease);
}

.card:hover,
.card:focus-within {
  transform: translateY(-4px);
  border-color: color-mix(in srgb, var(--sig) 32%, var(--line-2));
  box-shadow: var(--shadow-2);
}

.card:hover::after,
.card:focus-within::after { opacity: 1; }

.card h3 { letter-spacing: -0.024em; }
.card > * + * { margin-top: 12px; }

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

.mono {
  font-family: var(--font-mono);
  font-feature-settings: "liga" 0;
}

.grad-text {
  background: var(--grad-sig);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* Safari falls back to the solid colour without the prefixed fill. */
  -webkit-text-fill-color: transparent;
}

/* Both of these only do anything once site.js has run: the palette needs JS to
   open, and the theme toggle needs JS to write data-theme. Without it they are
   dead controls, so they are not offered. */
html:not(.js) [data-theme-toggle],
html:not(.js) [data-cmdk-open] { display: none; }

/* Present in base.html for keyboard users; only visible while focused. */
.skip-link {
  position: fixed;
  top: 10px;
  left: 10px;
  z-index: 400;
  padding: 10px 16px;
  border-radius: var(--radius-btn);
  background: var(--ink-2);
  border: 1px solid var(--sig);
  color: var(--fg);
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform var(--dur) var(--ease);
}

.skip-link:focus,
.skip-link:focus-visible {
  /* base.html gives this .sr-only as well, so focus has to undo every part of
     the clip, not just the transform, or the link "appears" as a 1px box. */
  transform: none;
  width: auto;
  height: auto;
  margin: 0;
  overflow: visible;
  clip-path: none;
}



/* home.html heads each focus card with a 24px line icon. It inherits the card's
   text colour by default, which reads as a second paragraph rather than a mark. */
.card-icon {
  color: var(--sig);
  margin-bottom: 16px;
}


/* ── 4. Nav ──────────────────────────────────────────────────────────────── */

.site-nav {
  position: sticky;
  top: 0;
  z-index: 100;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              backdrop-filter var(--dur) var(--ease);
}

/* JS adds .solid past 24px of scroll. Backdrop blur only once it is solid, so
   the hero canvas is not blurred through an invisible bar at rest. */
.site-nav.solid {
  background: color-mix(in srgb, var(--ink) 78%, transparent);
  backdrop-filter: saturate(150%) blur(14px);
  -webkit-backdrop-filter: saturate(150%) blur(14px);
  border-bottom-color: var(--line);
}

.nav-inner {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding: 14px 32px;
  display: flex;
  align-items: center;
  gap: 22px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -0.02em;
  color: var(--fg);
  text-decoration: none;
  white-space: nowrap;
}

.brand::before {
  content: "";
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--grad-sig);
  box-shadow: 0 0 0 3px var(--sig-wash);
  flex: none;
}

.brand:hover { color: var(--fg); }

.nav-links {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  list-style: none;
  padding: 0;
}

.nav-links li { margin: 0; }

.navlink {
  position: relative;
  display: inline-block;
  padding: 8px 12px;
  border-radius: var(--radius-chip);
  font-size: 14px;
  color: var(--fg-dim);
  text-decoration: none;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.navlink:hover {
  color: var(--fg);
  background: var(--sig-wash);
}

.navlink.active { color: var(--fg); }

.navlink.active::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 2px;
  height: 2px;
  border-radius: 2px;
  background: var(--grad-sig);
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Injected by JS, so it only exists on the enhanced page. Below 820px the
   enhanced nav collapses; the plain nav simply wraps and stays usable. 820 and
   not 760 because the full link row stops fitting at about 790, and it matches
   the width site.js uses to force the menu shut on resize. */
.nav-toggle {
  display: none;
  position: relative;
  flex: none;              /* it is the last item in the row: never let it shrink */
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-btn);
  background: var(--ink-2);
  color: var(--fg);
  cursor: pointer;
}

/* The bars are drawn by the button itself when the JS injects no children, and
   by the children when it does. :has() picks the right one at runtime. */
.nav-toggle:not(:has(*))::before,
.nav-toggle:not(:has(*))::after,
.nav-toggle > span {
  content: "";
  position: absolute;
  left: 50%;
  width: 17px;
  height: 1.5px;
  margin-left: -8.5px;
  border-radius: 2px;
  background: currentColor;
  transition: transform var(--dur) var(--ease), opacity 140ms linear;
}

.nav-toggle:not(:has(*))::before { top: 14px; }
.nav-toggle:not(:has(*))::after  { top: 24px; }

.nav-toggle > span:nth-child(1) { top: 13px; }
.nav-toggle > span:nth-child(2) { top: 19px; }
.nav-toggle > span:nth-child(3) { top: 25px; }

/* .menu-open may land on the nav or on the body; accept either. */
.site-nav.menu-open .nav-toggle:not(:has(*))::before,
body.menu-open .nav-toggle:not(:has(*))::before { transform: translateY(5px) rotate(45deg); }

.site-nav.menu-open .nav-toggle:not(:has(*))::after,
body.menu-open .nav-toggle:not(:has(*))::after { transform: translateY(-5px) rotate(-45deg); }

.site-nav.menu-open .nav-toggle > span:nth-child(1),
body.menu-open .nav-toggle > span:nth-child(1) { transform: translateY(6px) rotate(45deg); }

.site-nav.menu-open .nav-toggle > span:nth-child(2),
body.menu-open .nav-toggle > span:nth-child(2) { opacity: 0; transform: scaleX(0.2); }

.site-nav.menu-open .nav-toggle > span:nth-child(3),
body.menu-open .nav-toggle > span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }



.brand-name { font-weight: 600; }

/* The toggle ships both icons and shows one. site.js writes data-theme-state on
   the button; before it runs the media query picks the right one, so the button
   is never a sun and a moon at the same time. */
.icon-sun,
.icon-moon { grid-area: 1 / 1; }

[data-theme-toggle] {
  display: inline-grid;
  place-items: center;
}

.icon-moon { display: none; }

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .icon-sun  { display: none; }
  :root:not([data-theme="dark"]) .icon-moon { display: block; }
}

:root[data-theme="light"] .icon-sun  { display: none; }
:root[data-theme="light"] .icon-moon { display: block; }
:root[data-theme="dark"]  .icon-sun  { display: block; }
:root[data-theme="dark"]  .icon-moon { display: none; }

/* Once site.js has run its attribute is the authority, whatever the OS says. */
[data-theme-state="dark"]  .icon-sun  { display: block; }
[data-theme-state="dark"]  .icon-moon { display: none; }
[data-theme-state="light"] .icon-sun  { display: none; }
[data-theme-state="light"] .icon-moon { display: block; }


/* ── 4b. Language switcher and suggestion banner ─────────────────────────── */

/* The switcher is server-rendered, one real <a> per locale, and works with JS
   off. site.js only adds data-short, aria-current and a memory of the choice.
   Styled as a segmented control because a language is a state you are in, not
   an action you take. */
.lang-switch {
  display: inline-flex;
  align-items: center;
  flex: none;
  gap: 2px;
  margin: 0;
  padding: 2px;
  list-style: none;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-chip);
  background: color-mix(in srgb, var(--ink-2) 60%, transparent);
}

/* Rendered as a <ul> or as bare anchors; either way the anchors are the row. */
.lang-switch li { display: flex; margin: 0; }

.lang-switch a {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 5px 10px;
  border-radius: calc(var(--radius-chip) - 2px);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.05em;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  color: var(--fg-mute);
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.lang-switch a:hover { color: var(--fg); background: var(--sig-wash); }

/* The template may set aria-current="page"; site.js sets "true" when it did
   not. Matching the bare attribute accepts both. */
.lang-switch a[aria-current] {
  color: var(--sig-ink);
  background: var(--sig-wash);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sig) 30%, transparent);
}

/* Injected by site.js, never server-rendered: with JS off there is no banner
   and the switcher is the whole feature. It arrives after first paint because
   the script is deferred, so it opens from zero height rather than appearing
   at full size, which reads as arriving rather than as a layout glitch. */
.lang-banner {
  position: relative;
  z-index: 90;                    /* below the sticky nav, above page content */
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  background: var(--ink-2);
  border-bottom: 1px solid var(--line);
  transition: max-height 320ms var(--ease-out), opacity 200ms linear;
}

/* Generous enough for the sentence to wrap to three lines on a phone. */
.lang-banner.is-in { max-height: 9rem; opacity: 1; }

.lang-banner-inner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-block: 9px;
}

.lang-banner-text {
  margin: 0;
  min-width: 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--fg-dim);
}

.lang-banner-go {
  flex: none;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 10px;
  border-radius: var(--radius-btn);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  color: var(--sig-ink);
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.lang-banner-go::after { content: "→"; }
.lang-banner-go:hover { color: var(--sig); background: var(--sig-wash); }

.lang-banner-x {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-chip);
  background: transparent;
  color: var(--fg-mute);
  font: inherit;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.lang-banner-x:hover { color: var(--fg); background: var(--sig-wash); }


/* ── 5. Buttons ──────────────────────────────────────────────────────────── */

.btn {
  --btn-lift: 0;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 12px 20px;
  border: 1px solid transparent;
  border-radius: var(--radius-btn);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  background: transparent;
  color: var(--fg);
  transform: translateY(var(--btn-lift));
  transition: transform var(--dur) var(--ease),
              background var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}

/* site.js writes the magnetic offset to `translate`, a separate property from
   `transform`, so the two compose and neither has to know about the other. */
.btn[data-magnetic] { will-change: transform, translate; }

.btn:hover { --btn-lift: -2px; }
.btn:active { --btn-lift: 0px; }

/* The label is --ink and the fill is pushed 20% towards --fg, which is the only
   formulation that clears 4.5:1 across the whole gradient in both themes: raw
   --grad-sig leaves dark-on-indigo at 2.6:1 in the light theme. Both halves
   move with the tokens, so there is still nothing theme-specific here. */
.btn-sig {
  background: linear-gradient(120deg,
    color-mix(in oklab, var(--sig) 80%, var(--fg)),
    color-mix(in oklab, var(--sig-2) 80%, var(--fg)));
  color: var(--ink);
  box-shadow: 0 10px 30px -14px color-mix(in oklab, var(--sig) 70%, transparent);
}

.btn-sig:hover {
  color: var(--ink);
  box-shadow: 0 16px 40px -14px color-mix(in oklab, var(--sig) 85%, transparent);
}

/* The gradient is fixed to the element, so a sheen has to be a second layer. */
.btn-sig::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(120deg, transparent 30%,
    rgba(255, 255, 255, 0.35) 50%, transparent 70%);
  background-size: 260% 100%;
  background-position: 140% 0;
  opacity: 0;
  transition: background-position 700ms var(--ease-out), opacity 200ms linear;
  pointer-events: none;
}

.btn-sig:hover::after { opacity: 1; background-position: -40% 0; }

.btn-ghost {
  border-color: var(--line-2);
  background: color-mix(in srgb, var(--ink-2) 70%, transparent);
  color: var(--fg);
}

.btn-ghost:hover {
  border-color: color-mix(in srgb, var(--sig) 55%, var(--line-2));
  background: var(--sig-wash);
  color: var(--fg);
}

.btn-quiet {
  padding-inline: 10px;
  color: var(--fg-dim);
}

.btn-quiet:hover { color: var(--sig); background: var(--sig-wash); }

.btn[aria-pressed="true"],
.btn.is-active {
  border-color: color-mix(in srgb, var(--sig) 55%, var(--line-2));
  color: var(--sig-ink);
  background: var(--sig-wash);
}

.btn-icon {
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: var(--radius-btn);
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-top: 30px;
}


/* ── 6. Kickers, chips, status pills ─────────────────────────────────────── */

.kicker {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--tracking-kicker);
  color: var(--fg-mute);
}

.kicker::before {
  content: "";
  width: 22px;
  height: 1px;
  background: var(--sig);
  flex: none;
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  list-style: none;
  padding: 0;
}

.chip-row li { margin: 0; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-chip);
  background: var(--ink-3);
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--fg-dim);
  white-space: nowrap;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.chip:hover {
  border-color: color-mix(in srgb, var(--sig) 40%, var(--line-2));
  color: var(--fg);
}

.pill-live {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px 6px 10px;
  border: 1px solid color-mix(in srgb, var(--live) 34%, var(--line));
  border-radius: 99px;
  background: color-mix(in oklab, var(--live) 10%, transparent);
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
  color: var(--fg);
}

.pill-live::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--live);
  box-shadow: 0 0 0 0 color-mix(in oklab, var(--live) 60%, transparent);
  animation: pulse-dot 2.4s var(--ease-out) infinite;
  flex: none;
}


/* ── 7. Hero ─────────────────────────────────────────────────────────────── */

.hero {
  position: relative;
  isolation: isolate;
  padding: clamp(72px, 12vh, 132px) 0 clamp(56px, 9vh, 96px);
  overflow: clip;
}

#hero-net {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  opacity: 0.85;
  pointer-events: none;
  /* Fade the node field out where the copy sits so the text never fights it. */
  mask-image: radial-gradient(120% 100% at 78% 34%,
    #000 0%, #000 42%, transparent 78%);
  -webkit-mask-image: radial-gradient(120% 100% at 78% 34%,
    #000 0%, #000 42%, transparent 78%);
}

.hero-inner {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  /* An explicit row track is what makes grid-row: 1 / -1 resolvable on the
     terminal: with no explicit rows, line -1 is line 1 and the span collapses.
     Four rows with the availability badge, three without it. */
  grid-template-rows: repeat(4, auto);
  align-items: center;
  column-gap: 56px;
  row-gap: 0;
}

.hero-inner:not(:has(> .hero-badge)) { grid-template-rows: repeat(3, auto); }

.hero-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 24px;
}

.hero h1 { margin-bottom: 20px; }

.hero .lead { max-width: 54ch; }

.hero-terminal { min-width: 0; }



/* .hero-inner takes flat children (badge, h1, lead, cta-row, terminal), so the
   copy has to be pinned to column one or auto-placement deals it alternately
   into both columns. */
.hero-inner > * { grid-column: 1; }

.hero-terminal {
  grid-column: 2;
  grid-row: 1 / -1;
  align-self: center;
}


/* ── 8. Terminal ─────────────────────────────────────────────────────────── */

.term {
  border: 1px solid var(--line-2);
  border-radius: var(--radius-panel);
  background: color-mix(in srgb, var(--ink-2) 92%, transparent);
  box-shadow: var(--shadow-2);
  overflow: hidden;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.term-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 14px;
  background: var(--ink-3);
  border-bottom: 1px solid var(--line);
}

/* The three dots are markup when the template supplies them and drawn here
   when it does not, so the chrome is correct either way. */
.term-bar:not(:has(.term-dot))::before {
  content: "";
  width: 44px;
  height: 10px;
  flex: none;
  background:
    radial-gradient(circle 5px at 5px 5px,  #FF5F57 98%, transparent) 0 0 / 17px 10px no-repeat,
    radial-gradient(circle 5px at 5px 5px,  #FEBC2E 98%, transparent) 17px 0 / 17px 10px no-repeat,
    radial-gradient(circle 5px at 5px 5px,  #28C840 98%, transparent) 34px 0 / 10px 10px no-repeat;
}

.term-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--line-2);
  flex: none;
}

.term-dot:nth-of-type(1) { background: #FF5F57; }
.term-dot:nth-of-type(2) { background: #FEBC2E; }
.term-dot:nth-of-type(3) { background: #28C840; }

/* ── link / activity readout ─────────────────────────────────────────────
   The LED on an ethernet port, doing the job it actually does: --lit is driven
   from real character events in site.js, one pulse per character, decaying over
   ~150ms. At the typewriter's ~28ms a character that reads as mostly-lit with
   small dips, which is what a NIC under sustained load looks like, because it
   is the same mechanism. It is not a loop and it is not random: when the last
   line lands, .is-linked takes over and it sits steady for good.

   Deliberately NOT an ambient flicker. Nothing on this page should blink
   forever, both because peripheral vision cannot ignore it and because a light
   reporting nothing is exactly the fake telemetry this site's whole premise is
   against. See .claude/decisions/2026-09-05-terminal-activity-led.md. */
.term-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-left: auto;
  font-size: 10px;
  letter-spacing: 0.16em;
  color: var(--fg-mute);
  flex: none;
}

.term-led {
  --lit: 0;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex: none;
  /* Resting state is a dark, unlit lamp, so the pulse reads as the lamp coming
     on rather than as a dot changing colour. */
  background: color-mix(in oklab, var(--live) calc(14% + var(--lit) * 86%), transparent);
  box-shadow: 0 0 calc(2px + var(--lit) * 7px)
              color-mix(in oklab, var(--live) calc(var(--lit) * 70%), transparent);
  transition: background 150ms linear, box-shadow 150ms linear;
}

/* Link up, no traffic: steady and dim, the way a port sits when it is connected
   and idle. This is the state it spends the rest of the visit in. */
.term-link.is-linked .term-led {
  --lit: 0.42;
  transition: background 400ms var(--ease), box-shadow 400ms var(--ease);
}

.term-title {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--fg-mute);
  letter-spacing: 0.02em;
  margin-inline: auto;
  padding-right: 44px;   /* optically centres against the dots */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.term-body {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.75;
  padding: 18px 18px 20px;
  min-height: 232px;
  overflow-x: auto;
}

/* Wrap rather than scroll, so nothing is hidden on a phone, and hang the
   continuation so a wrapped line still reads as one command.

   `normal` on the line, `pre-wrap` on the output span: the prompt and the
   command sit on separate source lines in home.html for readability, and a
   pre-wrap line renders that newline and its indentation, which put every
   command on its own line under a stray ten-space indent. Collapsing at the
   line and preserving inside .term-out keeps typed output's spacing without
   letting the template's own formatting reach the screen. */
.term-line {
  display: block;
  white-space: normal;
  word-break: break-word;
  padding-left: 1.4em;
  text-indent: -1.4em;
}

.term-line + .term-line { margin-top: 2px; }

.term-ps1 {
  color: var(--sig-ink);
  user-select: none;
}

.term-ps1::after {
  content: " ";
  white-space: pre;
}

.term-out {
  color: var(--fg-dim);
  white-space: pre-wrap;
}

/* Status colours are picked for a dark ground, so on the light theme they land
   near 1.6:1. Mixing towards --fg darkens them on light and lightens them on
   dark, which is the correct direction for both without a theme block. */
.term-out .ok   { color: color-mix(in oklab, var(--live) 55%, var(--fg)); }
.term-out .warn { color: color-mix(in oklab, var(--warn) 55%, var(--fg)); }
.term-out .crit { color: color-mix(in oklab, var(--crit) 62%, var(--fg)); }

/* step-end so the caret snaps between states instead of fading, which is what
   a real terminal does. */
[data-typewriter].is-typing::after,
.term-caret::after {
  content: "";
  display: inline-block;
  width: 0.55em;
  height: 1.05em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: var(--sig);
  animation: caret-blink 1.05s step-end infinite;
}



/* home.html supplies one empty .term-dots span rather than three .term-dot
   children, so paint the chrome on it and stand the ::before fallback down. */
.term-bar:has(.term-dots)::before { content: none; }

.term-dots {
  width: 44px;
  height: 10px;
  flex: none;
  background:
    radial-gradient(circle 5px at 5px 5px,  #FF5F57 98%, transparent) 0 0 / 17px 10px no-repeat,
    radial-gradient(circle 5px at 5px 5px,  #FEBC2E 98%, transparent) 17px 0 / 17px 10px no-repeat,
    radial-gradient(circle 5px at 5px 5px,  #28C840 98%, transparent) 34px 0 / 10px 10px no-repeat;
}


/* ── 9. Stats ────────────────────────────────────────────────────────────── */

.stat-row {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 18px;
  padding: 30px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.stat { min-width: 0; }

.stat-num {
  display: block;
  font-size: clamp(32px, 4.2vw, 46px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: var(--tracking-display);
  font-variant-numeric: tabular-nums;
  background: var(--grad-sig);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

/* Wherever the count-up runs, the digits have to be the same width or the
   number jitters its own layout for the whole 1100ms. */
[data-count-to] { font-variant-numeric: tabular-nums; }

.stat-label {
  display: block;
  margin-top: 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--fg-dim);
}


/* ── 10. Timeline ────────────────────────────────────────────────────────── */

.timeline {
  --rail: 19px;      /* x position of the spine inside the timeline */
  position: relative;
  padding-left: 56px;
}

/* The unlit rail. The SVG spine draws the lit accent line over the top of it. */
.timeline::before {
  content: "";
  position: absolute;
  left: var(--rail);
  top: 6px;
  bottom: 6px;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg,
    transparent 0%, var(--line-2) 6%, var(--line-2) 94%, transparent 100%);
}

/* A highlight that travels the rail, so the eye reads the timeline as a live
   link rather than a static list. */
.timeline::after {
  content: "";
  position: absolute;
  left: calc(var(--rail) - 3px);
  top: 0;
  width: 8px;
  height: 140px;
  border-radius: 8px;
  pointer-events: none;
  background: linear-gradient(180deg,
    transparent 0%,
    color-mix(in oklab, var(--sig) 55%, transparent) 45%,
    transparent 100%);
  filter: blur(4px);
  animation: spine-travel 7.5s linear infinite;
}

.timeline-spine {
  position: absolute;
  left: var(--rail);
  top: 0;
  width: 2px;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

#spine-path {
  fill: none;
  stroke: var(--sig);
  stroke-width: 2;
  stroke-linecap: round;
  filter: drop-shadow(0 0 6px color-mix(in oklab, var(--sig) 55%, transparent));
}

.tl-item {
  position: relative;
  padding-bottom: 30px;
}

.tl-item:last-child { padding-bottom: 0; }

.tl-dot {
  position: absolute;
  left: -44px;
  top: 26px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--ink);
  border: 2px solid var(--fg-mute);
  box-shadow: 0 0 0 5px var(--ink);
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}

.tl-item:hover .tl-dot,
.tl-item:focus-within .tl-dot {
  border-color: var(--sig);
  box-shadow: 0 0 0 5px var(--ink),
              0 0 18px color-mix(in oklab, var(--sig) 70%, transparent);
}

.tl-item.is-current .tl-dot {
  border-color: var(--live);
  animation: pulse-ring 2.6s var(--ease-out) infinite;
}

.tl-card {
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 24px 26px 26px;
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}

.tl-card:hover {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--sig) 30%, var(--line-2));
  box-shadow: var(--shadow-1);
}

.tl-when {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--fg-dim);
}

.tl-when > * + *::before {
  content: "·";
  margin-right: 8px;
  color: var(--line-2);
}

.tl-role {
  margin-top: 10px;
  font-size: clamp(19px, 2vw, 23px);
  font-weight: 600;
  letter-spacing: -0.024em;
}

.tl-org {
  margin-top: 2px;
  font-size: 15px;
  font-weight: 500;
  color: var(--sig-ink);
}

.tl-place {
  margin-top: 2px;
  font-size: 13px;
  color: var(--fg-dim);
}

.tl-summary {
  margin-top: 14px;
  color: var(--fg-dim);
  max-width: 68ch;
}

.tl-highlights {
  margin-top: 16px;
  padding-left: 0;
  list-style: none;
  display: grid;
  gap: 8px;
}

.tl-highlights li {
  position: relative;
  padding-left: 22px;
  margin: 0;
  color: var(--fg-dim);
  font-size: 15px;
}

.tl-highlights li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 0.62em;
  width: 7px;
  height: 7px;
  border-radius: 2px;
  background: var(--grad-sig);
}

.tl-card .chip-row { margin-top: 18px; }

.tl-duties {
  margin-top: 18px;
  border-top: 1px solid var(--line);
  padding-top: 14px;
}

.tl-duties > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  list-style: none;
  font-family: var(--font-mono);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--fg-dim);
  transition: color var(--dur) var(--ease);
}

.tl-duties > summary::-webkit-details-marker { display: none; }

.tl-duties > summary::after {
  content: "";
  width: 7px;
  height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur) var(--ease);
}

.tl-duties[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.tl-duties > summary:hover { color: var(--sig); }

.tl-duties[open] > summary { margin-bottom: 14px; }

.tl-duties h4,
.tl-duties .duty-group > :first-child {
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--sig-ink);
  margin-top: 16px;
}

.tl-duties ul { color: var(--fg-dim); font-size: 14.5px; }



/* The same filter the skill matrix uses, for the timeline. experience.html puts
   data-active-filter on a wrapper around both the buttons and the items, and
   "all" matches none of these, which is what makes it the unfiltered state. */
[data-active-filter="platform"] .tl-item:not([data-focus~="platform"]),
[data-active-filter="cloud"]    .tl-item:not([data-focus~="cloud"]),
[data-active-filter="network"]  .tl-item:not([data-focus~="network"]),
[data-active-filter="security"] .tl-item:not([data-focus~="security"]),
[data-active-filter="support"]  .tl-item:not([data-focus~="support"]) {
  display: none;
}

/* .tl-when draws the separator between its children; these only set the weight. */
.tl-dur { color: var(--fg-mute); }

/* The item carries .is-current for the pulse; the dot carries .current as well,
   so name both or a markup change silently kills the live state. */
.tl-item.is-current .tl-dot,
.tl-dot.current {
  border-color: var(--live);
}

.tl-duties .duty-note {
  margin-top: 10px;
  font-size: 11.5px;
  color: var(--fg-mute);
}


/* ── 11. Skill matrix ────────────────────────────────────────────────────── */

.skills {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px;
}

.skill-group {
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 24px 26px 26px;
  transition: border-color var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

.skill-group:hover { border-color: var(--line-2); }
.skill-group[hidden] { display: none; }

/* Same deal for the skill matrix: the root carries data-active-filter, the
   groups carry their own tags, so this works server-side too. "all" matches
   none of these rules, which is what makes it the unfiltered state. */
.skills[data-active-filter="platform"] .skill-group:not([data-focus~="platform"]),
.skills[data-active-filter="cloud"]    .skill-group:not([data-focus~="cloud"]),
.skills[data-active-filter="network"]  .skill-group:not([data-focus~="network"]),
.skills[data-active-filter="security"] .skill-group:not([data-focus~="security"]) {
  display: none;
}

.skill-group > h3 {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  margin-bottom: 18px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}

.skill {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 92px 42px;
  align-items: center;
  gap: 14px;
  padding: 7px 0;
}

.skill-name {
  font-size: 14px;
  color: var(--fg-dim);
  min-width: 0;
}

.skill:hover .skill-name { color: var(--fg); }

.skill-meter {
  height: 6px;
  border-radius: 99px;
  background: var(--ink-3);
  border: 1px solid var(--line);
  overflow: hidden;
}

/* level / 5 * 100. Written as static widths so the CSS, not the JS, owns the
   geometry; the fill only needs a class flipped to animate to it. */
.skill-fill {
  --w: 0%;
  display: block;
  width: 0;
  height: 100%;
  border-radius: 99px;
  background: var(--grad-sig);
  box-shadow: 0 0 12px -2px color-mix(in oklab, var(--sig) 70%, transparent);
  transition: width 900ms var(--ease-out);
  transition-delay: calc(var(--d, 0) * 55ms);
}

.skill-fill[data-level="1"] { --w: 20%; }
.skill-fill[data-level="2"] { --w: 40%; }
.skill-fill[data-level="3"] { --w: 60%; }
.skill-fill[data-level="4"] { --w: 80%; }
.skill-fill[data-level="5"] { --w: 100%; }

/* Filled once anything above it reveals, or immediately with JS switched off. */
.in .skill-fill,
.skill-fill.in,
html:not(.js) .skill-fill { width: var(--w); }

.skill-years {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-dim);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 26px;
}

.filter-btn {
  padding: 8px 14px;
  border: 1px solid var(--line);
  border-radius: 99px;
  background: var(--ink-2);
  color: var(--fg-dim);
  font: inherit;
  font-family: var(--font-mono);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  cursor: pointer;
  transition: color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              background var(--dur) var(--ease);
}

.filter-btn:hover {
  color: var(--fg);
  border-color: var(--line-2);
}

.filter-btn[aria-pressed="true"],
.filter-btn.is-active {
  color: var(--sig-ink);
  border-color: color-mix(in srgb, var(--sig) 50%, var(--line-2));
  background: var(--sig-wash);
}



.filter-count {
  margin-left: 6px;
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
}

.filter-btn[aria-pressed="true"] .filter-count,
.filter-btn.is-active .filter-count { color: inherit; }


/* ── 12. Command palette ─────────────────────────────────────────────────── */

.cmdk {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: clamp(60px, 14vh, 140px) 20px 20px;
  border: 0;
  width: 100%;
  max-width: none;
  max-height: none;
  background: color-mix(in srgb, var(--ink) 62%, transparent);
  backdrop-filter: blur(10px) saturate(120%);
  -webkit-backdrop-filter: blur(10px) saturate(120%);
}

/* Two ways this can be closed: the hidden attribute before JS runs, and the
   dialog element's own closed state if base.html used <dialog>. */
.cmdk[hidden] { display: none !important; }
dialog.cmdk:not([open]) { display: none; }
.cmdk::backdrop { background: transparent; }

.cmdk-panel {
  width: min(620px, 100%);
  max-height: min(60vh, 480px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--ink-2);
  color: var(--fg);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-panel);
  box-shadow: var(--shadow-2),
              0 0 60px -20px color-mix(in oklab, var(--sig) 45%, transparent);
  animation: cmdk-in 200ms var(--ease-out) both;
}

.cmdk-input {
  flex: none;
  width: 100%;
  padding: 17px 20px;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: transparent;
  color: var(--fg);
  font: inherit;
  font-size: 16px;
  outline: none;
}

.cmdk-input::placeholder { color: var(--fg-mute); }

.cmdk-list {
  flex: 1 1 auto;
  overflow-y: auto;
  list-style: none;
  margin: 0;
  padding: 8px;
  overscroll-behavior: contain;
}

.cmdk-list li { margin: 0; }

.cmdk-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 10px 12px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--fg-dim);
  font: inherit;
  font-size: 14px;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.cmdk-item:hover { color: var(--fg); background: var(--ink-3); }

.cmdk-item.is-active {
  color: var(--fg);
  background: var(--sig-wash);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sig) 35%, transparent);
}

.cmdk-item .mono,
.cmdk-item small {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-mute);
}

.cmdk-empty {
  padding: 28px 20px;
  text-align: center;
  color: var(--fg-dim);
  font-size: 14px;
}

.cmdk-hint {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  padding: 10px 16px;
  border-top: 1px solid var(--line);
  background: var(--ink-3);
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-mute);
}



/* site.js flags <html> while the palette is up: the page behind a modal must
   not scroll under it. */
.cmdk-open,
.cmdk-open body { overflow: hidden; }

/* Set on the next frame after the palette is unhidden. The panel runs its own
   entrance animation, so this only has to hold the finished state. */
.cmdk.is-open { opacity: 1; }


/* ── 13. CV page ─────────────────────────────────────────────────────────── */

/* Built like .site-nav: a full-bleed sticky rail with a .wrap inside, so it
   carries a bottom hairline only. A box border and a radius would draw their
   corners at the viewport edges, and the row layout lives on .cv-toolbar-in. */
.cv-toolbar {
  position: sticky;
  top: 68px;
  z-index: 20;
  margin-bottom: 26px;
  padding-block: 12px;
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--ink-2) 88%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.cv-toolbar .kicker { margin-right: 4px; }
/* Pushes the actions to the right edge of the first row. Named explicitly
   rather than :last-child, because the note now follows them in source order
   and sits on its own row underneath. */
.cv-toolbar-in > .cv-actions { margin-left: auto; }

.cv-doc {
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: clamp(28px, 4vw, 52px);
}

.cv-section + .cv-section {
  margin-top: 34px;
  padding-top: 30px;
  border-top: 1px solid var(--line);
}

.cv-section > h2 {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--tracking-kicker);
  color: var(--sig-ink);
  margin-bottom: 20px;
}

.cv-role + .cv-role { margin-top: 22px; }

.cv-role h3 {
  font-size: 17px;
  font-weight: 600;
}

.cv-role .tl-when,
.cv-role .cv-meta {
  margin-top: 3px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--fg-dim);
}

.cv-role ul {
  margin-top: 10px;
  color: var(--fg-dim);
  font-size: 15px;
}

/* The focus filter is pure CSS: the toolbar sets one attribute and the roles
   carry their own tags, so filtering keeps working if the JS never loads and
   the attribute is set server-side. */
.cv-doc[data-cv-focus="platform"] .cv-role:not([data-focus~="platform"]),
.cv-doc[data-cv-focus="cloud"]    .cv-role:not([data-focus~="cloud"]),
.cv-doc[data-cv-focus="network"]  .cv-role:not([data-focus~="network"]),
.cv-doc[data-cv-focus="security"] .cv-role:not([data-focus~="security"]),
.cv-doc[data-cv-focus="support"]  .cv-role:not([data-focus~="support"]) {
  display: none;
}



/* The toolbar is a full-bleed sticky bar with its own .wrap inside, so the row
   layout belongs on that inner element, not on the bar. */
.cv-toolbar-in {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 28px;
}

.cv-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  min-width: 0;
}


.cv-filter-btns {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* Explanatory, not instructional: it must not compete with the buttons. Its own
   row under both, so its length can never size the row above it. */
.cv-filter-note {
  flex: 1 0 100%;
  margin: 0;
  font-size: 12px;
  color: var(--fg-mute);
}

.cv-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  flex: 0 0 auto;
  gap: 10px;
  margin-left: auto;
}

.cv-stamp {
  flex-basis: 100%;
  font-size: 11px;
  color: var(--fg-mute);
}

/* Inline icons sit inside a line of text, so they have to stop being blocks. */
.cv-doc .icon,
.cv-actions .icon {
  display: inline-block;
  vertical-align: -2px;
  flex: none;
}

.cv-head { margin-bottom: 30px; }

.cv-name {
  font-size: clamp(30px, 4.4vw, 44px);
  line-height: 1.02;
  letter-spacing: var(--tracking-display);
}

.cv-headline {
  margin-top: 6px;
  font-size: 16px;
  color: var(--sig-ink);
}

.cv-contact {
  margin-top: 16px;
  padding-left: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 22px;
  font-size: 12.5px;
  color: var(--fg-dim);
}

.cv-contact li {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 7px;
}

.cv-contact a { color: inherit; text-decoration: none; }
.cv-contact a:hover { color: var(--sig); }

.cv-status {
  margin-top: 12px;
  font-size: 12px;
  color: var(--fg-mute);
}

.cv-summary { color: var(--fg-dim); max-width: 76ch; }

/* A sub-heading inside a section, below the mono kicker that titles it. */
.cv-sub {
  margin-top: 18px;
  margin-bottom: 10px;
  font-size: 13px;
  color: var(--fg-dim);
}

.cv-section > .cv-sub:first-of-type { margin-top: 0; }

.cv-skill-list {
  padding-left: 0;
  list-style: none;
  gap: 4px 28px;
}

.cv-skill {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 5px 0;
  border-bottom: 1px solid var(--line);
}

.cv-skill-name { font-size: 14.5px; }

.cv-skill-years {
  margin-left: auto;
  font-size: 11px;
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
}

.cv-role-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 18px;
}

.cv-role-title { font-size: 17px; font-weight: 600; }
.cv-role-org { color: var(--sig-ink); }

/* Same treatment as .tl-when, which is the other rendering of the same fact. */
.cv-when {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 11.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--fg-dim);
}

.cv-dur { color: var(--fg-mute); }

.cv-role-meta {
  margin-top: 3px;
  font-size: 11.5px;
  color: var(--fg-mute);
}

.cv-role-summary {
  margin-top: 10px;
  color: var(--fg-dim);
  max-width: 76ch;
}

.cv-highlights {
  margin-top: 10px;
  padding-left: 1.1em;
  color: var(--fg-dim);
  font-size: 15px;
}

.cv-role-foot {
  margin-top: 14px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 18px;
}

.cv-role-link {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 11.5px;
  text-decoration: none;
  white-space: nowrap;
}

.cv-entry-list {
  padding-left: 0;
  list-style: none;
  display: grid;
  gap: 14px;
}

.cv-entry { margin: 0; }
.cv-entry-title { font-size: 15px; font-weight: 600; }

.cv-entry-meta {
  margin-top: 3px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  font-size: 11.5px;
  color: var(--fg-dim);
}

.cv-entry-note {
  margin-top: 4px;
  font-size: 12.5px;
  color: var(--fg-mute);
}

.cv-more { margin-top: 18px; }

.cv-more > summary {
  font-family: var(--font-mono);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--fg-mute);
}

.cv-more > summary:hover { color: var(--sig-ink); }
.cv-more[open] > summary { margin-bottom: 14px; }

.cv-colophon {
  margin-top: 34px;
  padding: 22px 24px;
  display: grid;
  gap: 10px;
  font-size: 13px;
  color: var(--fg-mute);
}


/* ── 14. Footer ──────────────────────────────────────────────────────────── */

.site-foot {
  margin-top: 96px;
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--ink-2) 55%, transparent);
}

.foot-inner {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding: 40px 32px;
  /* base.html emits a brand block and three link columns, not one row. */
  display: grid;
  grid-template-columns: minmax(220px, 1.5fr) repeat(3, minmax(130px, 1fr));
  align-items: start;
  gap: 28px 32px;
  font-size: 14px;
  color: var(--fg-dim);
}

.foot-links {
  display: flex;
  flex-direction: column;
  gap: 7px;
  list-style: none;
  padding: 0;
}

.foot-links li { margin: 0; }

.foot-links a {
  color: var(--fg-dim);
  text-decoration: none;
}

.foot-links a:hover { color: var(--sig); }

[data-year] { font-variant-numeric: tabular-nums; }



.foot-brand { display: grid; gap: 3px; }

.foot-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--fg);
}

.foot-brand a { color: var(--fg-dim); text-decoration: none; }
.foot-brand a:hover { color: var(--sig); }

.foot-col > .kicker { margin-bottom: 12px; }

/* Sits outside .foot-inner, so it carries the page gutter itself. */
.foot-base {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding: 18px 32px 34px;
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 28px;
  font-size: 12.5px;
  color: var(--fg-mute);
}

.foot-base p { max-width: 68ch; }

@media (max-width: 900px) {
  .foot-inner { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .foot-base { padding-inline: 24px; }
}

@media (max-width: 620px) {
  .foot-inner { grid-template-columns: minmax(0, 1fr); }
}


/* ── 15. JS-injected furniture ───────────────────────────────────────────── */

/* Width is written by site.js as a percentage of the read position. */
.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 200;
  width: 0;
  height: 2px;
  background: var(--grad-sig);
  box-shadow: 0 0 12px -2px color-mix(in oklab, var(--sig) 80%, transparent);
  pointer-events: none;
}

/* site.js may signal the shown state with .show, .is-visible or .in depending
   on how it ended up written; all three are accepted so the button can never
   be stranded invisible. */
.to-top {
  position: fixed;
  right: 22px;
  bottom: 22px;
  z-index: 90;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: 50%;
  background: color-mix(in srgb, var(--ink-2) 88%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: var(--fg-dim);
  cursor: pointer;
  opacity: 0;
  transform: translateY(12px);
  pointer-events: none;
  /* visibility, not just opacity: an opacity-0 button is still in the tab order,
     so a keyboard user would land on something nobody can see. Delayed off, so
     the fade out still plays before it is taken away. */
  visibility: hidden;
  transition: opacity var(--dur) var(--ease),
              transform var(--dur) var(--ease),
              color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              visibility 0s linear var(--dur);
}

.to-top[hidden] { display: none !important; }

.to-top.show,
.to-top.is-visible,
.to-top.in {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  visibility: visible;
  transition-delay: 0s;
}

.to-top:hover {
  color: var(--sig);
  border-color: color-mix(in srgb, var(--sig) 50%, var(--line-2));
}

.to-top::after {
  content: "";
  width: 9px;
  height: 9px;
  border-left: 1.5px solid currentColor;
  border-top: 1.5px solid currentColor;
  transform: rotate(45deg) translate(1px, 1px);
}

/* ── Portraits ───────────────────────────────────────────────────────────── */

/* One photo, two sizes, both circular. width/height are on the elements so the
   space is reserved before the file lands: this is the largest image on either
   page and a late reflow here would move the whole hero. */
.hero-portrait,
.cv-portrait {
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  background: var(--ink-3);
}

.hero-intro {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
}

.hero-portrait {
  width: 96px;
  height: 96px;
  /* A hairline plus the accent ring, so the photo reads as part of the system
     rather than as a picture dropped onto it. */
  border: 1px solid var(--line-2);
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--sig) 12%, transparent);
}

.cv-head {
  display: flex;
  align-items: flex-start;
  gap: 26px;
  flex-wrap: wrap;
}

.cv-head-text { flex: 1 1 320px; min-width: 0; }

.cv-portrait {
  width: 168px;
  height: 168px;
  border: 1px solid var(--line-2);
  box-shadow: 0 0 0 5px color-mix(in oklab, var(--sig) 10%, transparent);
}

@media (max-width: 560px) {
  .cv-portrait { width: 124px; height: 124px; }
  .hero-portrait { width: 76px; height: 76px; }
}


/* ── 16. Animations ──────────────────────────────────────────────────────── */

/* .reveal only hides once <html class="js"> is set, so a reader without
   JavaScript gets the finished page rather than an empty one. */
.js .reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 620ms var(--ease-out), transform 620ms var(--ease-out);
  transition-delay: calc(var(--d, 0) * 70ms);
  will-change: opacity, transform;
}

.js .reveal.in {
  opacity: 1;
  transform: none;
}

@keyframes caret-blink {
  0%, 50%   { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

@keyframes pulse-dot {
  0%   { box-shadow: 0 0 0 0 color-mix(in oklab, var(--live) 55%, transparent); }
  70%  { box-shadow: 0 0 0 7px color-mix(in oklab, var(--live) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--live) 0%, transparent); }
}

@keyframes pulse-ring {
  0%   { box-shadow: 0 0 0 5px var(--ink), 0 0 0 5px color-mix(in oklab, var(--live) 45%, transparent); }
  70%  { box-shadow: 0 0 0 5px var(--ink), 0 0 0 16px color-mix(in oklab, var(--live) 0%, transparent); }
  100% { box-shadow: 0 0 0 5px var(--ink), 0 0 0 0 color-mix(in oklab, var(--live) 0%, transparent); }
}

@keyframes spine-travel {
  0%   { top: -18%; opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { top: 104%; opacity: 0; }
}

@keyframes cmdk-in {
  from { opacity: 0; transform: translateY(-8px) scale(0.975); }
  to   { opacity: 1; transform: none; }
}


/* ── 17. Responsive ──────────────────────────────────────────────────────── */

@media (max-width: 1080px) {
  .hero-inner { column-gap: 40px; }
  .nav-inner { gap: 14px; }
  .navlink { padding-inline: 9px; }
  .section { padding: 76px 0; }
}

@media (max-width: 900px) {
  .wrap { padding-inline: 24px; }
  .nav-inner { padding-inline: 24px; }
  .foot-inner { padding-inline: 24px; }

  .grid-2 { grid-template-columns: minmax(0, 1fr); }
  .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .skills { grid-template-columns: minmax(0, 1fr); }

  /* The terminal drops below the copy rather than being squeezed beside it. */
  .hero-inner {
    grid-template-columns: minmax(0, 1fr);
    column-gap: 44px;
  }

  .hero-terminal {
    grid-column: 1;
    grid-row: auto;
    margin-top: 44px;
  }

  #hero-net {
    mask-image: radial-gradient(140% 80% at 50% 12%, #000 0%, #000 35%, transparent 80%);
    -webkit-mask-image: radial-gradient(140% 80% at 50% 12%, #000 0%, #000 35%, transparent 80%);
    opacity: 0.6;
  }

  .cv-toolbar { top: 62px; }
}

/* The nav collapses on its own breakpoint, above the 760px content one: brand +
   link row + actions overflow the bar at about 790px, and site.js already uses
   820 as the width at which an open menu is forced shut. Keeping the two apart
   is what closes the 761-800px band where the row ran off the right edge. */
@media (max-width: 820px) {
  .nav-enhanced .nav-toggle { display: inline-flex; }

  /* Enhanced nav: the links become a sheet under the bar. Without JS there is
     no .nav-enhanced class, so the links stay in the flow and simply wrap. */
  .nav-enhanced .nav-links {
    position: absolute;
    left: 12px;
    right: 12px;
    top: calc(100% + 8px);
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    margin: 0;
    padding: 10px;
    border: 1px solid var(--line-2);
    border-radius: var(--radius-panel);
    /* Fully opaque, not a tint: this sheet lands on top of the h1, and a
       backdrop filter is not reliable inside a bar that already has one. */
    background: var(--ink-2);
    box-shadow: var(--shadow-2);
    opacity: 0;
    transform: translateY(-8px);
    pointer-events: none;
    transition: opacity 180ms var(--ease), transform 180ms var(--ease);
  }

  .site-nav.nav-enhanced.menu-open .nav-links,
  body.menu-open .nav-enhanced .nav-links {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }

  .nav-enhanced .navlink {
    display: block;
    padding: 11px 12px;
    font-size: 15px;
  }

  .nav-enhanced .navlink.active::after { left: 12px; right: auto; width: 16px; }

  .site-nav { position: sticky; }
  .nav-inner { position: relative; }
  .nav-actions { margin-left: auto; }
  .nav-enhanced .nav-links { margin-left: 0; }

  /* Brand + theme toggle + download + switcher is the tightest the bar ever
     gets, and this is the width where it stops fitting. Native names stay. */
  .lang-switch { gap: 0; }
  .lang-switch a { padding-inline: 7px; }
}

@media (max-width: 760px) {
  .section { padding: 64px 0; }
  .grid-3 { grid-template-columns: minmax(0, 1fr); }

  .stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 18px; }

  .timeline { padding-left: 40px; --rail: 13px; }
  .tl-dot { left: -33px; top: 22px; }
  .tl-card { padding: 20px 20px 22px; }

  .skill { grid-template-columns: minmax(0, 1fr) 70px 38px; gap: 10px; }

  .cv-toolbar { top: 58px; }
  .cv-toolbar-in > .cv-actions { margin-left: 0; }

  .foot-inner { flex-direction: column; align-items: flex-start; }
  .foot-links { margin-left: 0; }
}

/* The switcher costs the bar about what the Download CV button does, and the
   two together run off the right edge somewhere around 575px, before the 560
   rules below have a chance to help. The download also sits in the hero, the
   footer, the palette and the CV toolbar; the switcher sits nowhere else, so
   the download is the one that goes, and only on a page that has a switcher. */
@media (max-width: 640px) {
  .nav-actions:has(.lang-switch) .btn-sig { display: none; }
}

@media (max-width: 560px) {
  .wrap { padding-inline: 18px; }
  .nav-inner { padding-inline: 18px; }
  .foot-inner { padding-inline: 18px; }

  /* The 128px Download CV button is what pushed the injected .nav-toggle past
     the right edge on a phone, and body{overflow-x:clip} then hid it, leaving
     no way to open the menu. The same download sits in the hero, the footer,
     the command palette and the CV toolbar, so the bar can lose it. */
  .nav-actions .btn-sig { display: none; }

  .section { padding: 52px 0; }
  .cta-row > .btn { flex: 1 1 auto; }

  .term-body { font-size: 12px; padding: 14px; min-height: 200px; }
  .term-title { display: none; }

  .stat-row { padding: 24px 0; }

  /* The meter is the least useful column on a phone; the number stays. */
  .skill { grid-template-columns: minmax(0, 1fr) 40px; }
  .skill-meter { grid-column: 1 / -1; grid-row: 2; margin-top: 4px; }

  .cmdk { padding-top: 12vh; }
  .cmdk-hint { display: none; }

  .to-top { right: 14px; bottom: 14px; }

  /* "Nederlands" next to "English" is wider than a phone bar can spare, so the
     switcher falls back to the two-letter code in data-short (the template's,
     or the one site.js derives from hreflang). The label is pushed out of a
     clipped box rather than hidden, so it is still read out and still copied;
     only anchors that actually have a code collapse, so a template that never
     supplies one keeps its full names instead of going blank. */
  .lang-switch a[data-short] {
    display: block;
    width: 2.6rem;
    padding-inline: 0;
    overflow: hidden;
    text-indent: 120%;
  }

  .lang-switch a[data-short]::after {
    content: attr(data-short);
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    text-indent: 0;
  }

  .lang-banner-inner { flex-wrap: wrap; gap: 8px 10px; }
  .lang-banner-text { flex: 1 1 100%; order: 1; }
  .lang-banner-go { order: 2; margin-left: 0; }
  .lang-banner-x { order: 3; margin-left: auto; }
}


/* ── 18. Reduced motion ──────────────────────────────────────────────────── */

/* The rule the accessibility floor turns on: with motion reduced the page must
   be complete and static, never blank. Everything that animates is zeroed and
   every reveal-gated element is forced to its finished state. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  html { scroll-behavior: auto; }

  .js .reveal,
  .js .reveal.in {
    opacity: 1 !important;
    transform: none !important;
  }

  .skill-fill { width: var(--w) !important; }

  .btn:hover,
  .card:hover,
  .tl-card:hover {
    transform: none !important;
  }

  .timeline::after { display: none; }
  .btn-sig::after { display: none; }

  /* No pulse; the typewriter fills instantly under reduced motion, so the lamp
     goes straight to its steady linked state. */
  .term-led { --lit: 0.42; transition: none; }

  /* The caret would otherwise sit at whichever half of the blink it froze on. */
  [data-typewriter].is-typing::after,
  .term-caret::after { opacity: 1; animation: none !important; }

  /* No slide. site.js adds .is-in on the same tick under reduced motion, so the
     banner is simply there; max-height goes to none in case the copy wraps
     further than the animated ceiling allows. */
  .lang-banner { transition: none !important; }
  .lang-banner.is-in { max-height: none; }
}


/* ── 19. Printing a site page ────────────────────────────────────────────── */

/* Only for someone hitting Cmd-P on a normal page. The three PDF sheets are
   templates/print/* and are dressed entirely by assets/print.css. */
@media print {
  html, body {
    background: #FFFFFF !important;
    color: #000000 !important;
    font-size: 11pt;
  }

  body::before,
  .site-nav,
  .site-foot,
  .cmdk,
  .cv-toolbar,
  .scroll-progress,
  .to-top,
  .skip-link,
  .filter-row,
  .term,
  .lang-switch,
  .lang-banner,
  canvas,
  #hero-net {
    display: none !important;
  }

  .wrap { max-width: none; padding: 0; }
  .section { padding: 0 0 18pt; }
  .hero { padding: 0 0 18pt; }

  .card,
  .panel,
  .tl-card,
  .cv-doc,
  .skill-group {
    background: #FFFFFF !important;
    border: 1px solid #CCCCCC !important;
    box-shadow: none !important;
    transform: none !important;
    border-radius: 0 !important;
    padding: 8pt 10pt;
    break-inside: avoid;
  }

  a { color: #000000 !important; text-decoration: underline; }

  /* Paper cannot follow a link, so print the destination once. Skipped for
     in-page anchors and for links whose text is already the URL. */
  .cv-doc a[href^="http"]::after,
  .tl-card a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 9pt;
    color: #444444;
    word-break: break-all;
  }

  h1 { font-size: 22pt; }
  h2 { font-size: 15pt; }
  h3 { font-size: 12pt; }

  h1, h2, h3, .tl-role, .cv-section > h2 {
    color: #000000 !important;
    -webkit-text-fill-color: #000000 !important;
    background: none !important;
    break-after: avoid;
  }

  .lead,
  .tl-summary,
  .tl-highlights li,
  .cv-role ul,
  .skill-name,
  .term-out { color: #222222 !important; }

  .kicker,
  .tl-when,
  .stat-label,
  .tl-place { color: #555555 !important; }

  .kicker::before { background: #555555 !important; }

  .stat-num {
    color: #000000 !important;
    -webkit-text-fill-color: #000000 !important;
    background: none !important;
  }

  .chip {
    background: #FFFFFF !important;
    border-color: #BBBBBB !important;
    color: #333333 !important;
  }

  .skill-meter { border-color: #BBBBBB !important; background: #EEEEEE !important; }
  .skill-fill { background: #666666 !important; box-shadow: none !important; }

  .timeline { padding-left: 0; }
  .timeline::before,
  .timeline::after,
  .timeline-spine,
  .tl-dot { display: none !important; }

  .tl-item { break-inside: avoid; padding-bottom: 10pt; }
  .cv-role { break-inside: avoid; }

  /* Everything the reader collapsed is still part of the record on paper. */
  details { display: block !important; }
  details > *:not(summary) { display: revert !important; }
  .tl-duties > summary::after { display: none; }
}
