/* ==========================================================================
   ennogelhaus.de — public stylesheet

   Dark-first. Light mode follows the OS unless the visitor overrides it with
   the header toggle, which sets data-theme on <html>.

   No web fonts, no icon fonts, no third-party assets: the page makes zero
   external requests, so nothing leaks a visitor's IP to another origin and
   nothing can break because someone else's CDN is down.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  color-scheme: dark;

  --bg: #0a0c10;
  --bg-sunken: #07090c;
  --surface: #10141b;
  --surface-2: #161b24;
  --surface-3: #1c222d;

  --line: #232a36;
  --line-soft: #1a1f28;
  --line-strong: #303947;

  --text: #e8ecf3;
  --text-strong: #ffffff;
  --muted: #97a3b6;
  --dim: #6a7688;

  --accent: #5c9dff;
  --accent-strong: #7db0ff;
  --accent-ink: #04101f;
  --accent-wash: rgba(92, 157, 255, 0.12);
  --accent-line: rgba(92, 157, 255, 0.32);

  --ok: #3ecf8e;
  --ok-wash: rgba(62, 207, 142, 0.12);
  --warn: #f0b429;
  --warn-wash: rgba(240, 180, 41, 0.12);
  --crit: #ff5d6c;
  --crit-wash: rgba(255, 93, 108, 0.12);

  --focus: #8cc0ff;

  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  --radius: 10px;
  --radius-lg: 14px;

  /* Full width. A fixed cap left several hundred pixels of dead gutter down
     either side of a wide display, which is space the page should be using.
     The padding-inline on .container is what still keeps text off the glass. */
  --container: 100%;
  /* Measure for long-form text. Wide enough to use the column it sits in
     rather than stopping halfway across the screen, but still short enough
     that the eye finds the next line. */
  --prose: 90ch;
  /* Width of the label column that runs down the left of every section:
     eyebrows, entry titles, skill categories. One rail, so the page has a
     consistent spine instead of each block guessing its own indent. */
  --rail: 15rem;

  --step: clamp(3rem, 6vw, 5.5rem);

  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px -12px rgba(0, 0, 0, 0.6);
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;

    --bg: #fbfcfe;
    --bg-sunken: #f2f5f9;
    --surface: #ffffff;
    --surface-2: #f7f9fc;
    --surface-3: #eef2f7;

    --line: #dfe5ee;
    --line-soft: #eaeff6;
    --line-strong: #c5cfdd;

    --text: #131a24;
    --text-strong: #000000;
    --muted: #556174;
    --dim: #798394;

    --accent: #0b5fd0;
    --accent-strong: #0949a3;
    --accent-ink: #ffffff;
    --accent-wash: rgba(11, 95, 208, 0.08);
    --accent-line: rgba(11, 95, 208, 0.28);

    --ok: #0f8a55;
    --ok-wash: rgba(15, 138, 85, 0.1);
    --warn: #9a6800;
    --warn-wash: rgba(154, 104, 0, 0.1);
    --crit: #c92a3a;
    --crit-wash: rgba(201, 42, 58, 0.09);

    --focus: #0b5fd0;

    --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 10px 28px -16px rgba(16, 24, 40, 0.28);
  }
}

:root[data-theme="light"] {
  color-scheme: light;

  --bg: #fbfcfe;
  --bg-sunken: #f2f5f9;
  --surface: #ffffff;
  --surface-2: #f7f9fc;
  --surface-3: #eef2f7;

  --line: #dfe5ee;
  --line-soft: #eaeff6;
  --line-strong: #c5cfdd;

  --text: #131a24;
  --text-strong: #000000;
  --muted: #556174;
  --dim: #798394;

  --accent: #0b5fd0;
  --accent-strong: #0949a3;
  --accent-ink: #ffffff;
  --accent-wash: rgba(11, 95, 208, 0.08);
  --accent-line: rgba(11, 95, 208, 0.28);

  --ok: #0f8a55;
  --ok-wash: rgba(15, 138, 85, 0.1);
  --warn: #9a6800;
  --warn-wash: rgba(154, 104, 0, 0.1);
  --crit: #c92a3a;
  --crit-wash: rgba(201, 42, 58, 0.09);

  --focus: #0b5fd0;

  --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 10px 28px -16px rgba(16, 24, 40, 0.28);
}

/* --------------------------------------------------------------------------
   2. Reset and base
   -------------------------------------------------------------------------- */

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

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

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

h1,
h2,
h3,
h4 {
  color: var(--text-strong);
  font-weight: 600;
  letter-spacing: -0.018em;
  line-height: 1.15;
  margin: 0;
  text-wrap: balance;
}

p {
  margin: 0;
  text-wrap: pretty;
}

a {
  color: var(--accent);
  text-decoration-color: var(--accent-line);
  text-underline-offset: 0.2em;
}

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

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

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

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

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: 3px;
}

::selection {
  background: var(--accent-wash);
  color: var(--text-strong);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.7rem 1rem;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
  border-radius: 0 0 var(--radius) 0;
}

.skip-link:focus {
  left: 0;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   3. Layout primitives
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: clamp(1.1rem, 4vw, 2.5rem);
}

.section {
  padding-block: var(--step);
  border-top: 1px solid var(--line-soft);
}

.section--flush {
  border-top: 0;
}

.section--sunken {
  background: var(--bg-sunken);
}

.stack > * + * {
  margin-top: var(--stack-gap, 1rem);
}

.prose {
  max-width: var(--prose);
}

/* Eyebrow: mono, numbered, hairline-led. The signature motif of the site. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 0 0 1.1rem;
}

.eyebrow::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* Long-form section: the label takes the rail and the prose takes the rest of
   the row. Without this the prose stops at its measure and leaves the right
   half of the container empty. */
.section-split {
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: start;
}

.section-split > .eyebrow {
  margin-bottom: 0;
  position: sticky;
  top: 88px;
}

/* The measure is the column now, so the inner cap would only reintroduce the
   gap it is there to close. */
.section-split .rich,
.section-split .prose,
.section-split .section-head p {
  max-width: none;
}

/* Once the row is this wide the line needs a little more size and leading to
   stay comfortable to read. */
@media (min-width: 1180px) {
  .section-split .rich {
    font-size: 1.04rem;
    line-height: 1.75;
  }
}

@media (max-width: 900px) {
  .section-split {
    grid-template-columns: 1fr;
    gap: 1.1rem;
  }

  .section-split > .eyebrow {
    position: static;
    margin-bottom: 0.4rem;
  }
}

.section-head {
  margin-bottom: 2.5rem;
}

.section-head h2 {
  font-size: clamp(1.5rem, 3vw, 2.1rem);
}

.section-head p {
  color: var(--muted);
  margin-top: 0.75rem;
  max-width: var(--prose);
}

/* --------------------------------------------------------------------------
   4. Header
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid var(--line-soft);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 64px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--text-strong);
  text-decoration: none;
  font-size: 0.98rem;
}

.brand__mark {
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 7px;
  background: var(--accent);
  color: var(--accent-ink);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: 0.15rem;
}

.site-nav a {
  padding: 0.45rem 0.7rem;
  border-radius: 7px;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.9rem;
  font-weight: 500;
  white-space: nowrap;
}

.site-nav a:hover {
  color: var(--text);
  background: var(--surface-2);
}

.site-nav a[aria-current="page"] {
  color: var(--text-strong);
  background: var(--surface-2);
}

.header-tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.icon-button {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--muted);
  border-radius: 8px;
  cursor: pointer;
  padding: 0;
}

.icon-button:hover {
  color: var(--text);
  border-color: var(--line-strong);
}

.icon-button svg {
  width: 16px;
  height: 16px;
}

/* The theme toggle swaps which glyph is visible. */
[data-theme="light"] .icon-button__moon,
:root:not([data-theme="dark"]) .icon-button__moon {
  display: none;
}

[data-theme="light"] .icon-button__sun,
:root:not([data-theme="dark"]) .icon-button__sun {
  display: block;
}

.icon-button__sun {
  display: none;
}

[data-theme="dark"] .icon-button__moon {
  display: block;
}

[data-theme="dark"] .icon-button__sun {
  display: none;
}

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

  :root:not([data-theme="light"]) .icon-button__sun {
    display: none;
  }
}

.nav-toggle {
  display: none;
}

@media (max-width: 860px) {
  .nav-toggle {
    display: grid;
  }

  .site-nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0.5rem clamp(1.1rem, 4vw, 2.5rem) 1rem;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
  }

  .site-nav[data-open="true"] {
    display: flex;
  }

  .site-nav a {
    padding: 0.7rem 0.6rem;
    border-bottom: 1px solid var(--line-soft);
    border-radius: 0;
  }
}

/* --------------------------------------------------------------------------
   5. Buttons and chips
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.62rem 1.05rem;
  border-radius: 9px;
  border: 1px solid transparent;
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  font-family: inherit;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.btn--primary {
  background: var(--accent);
  color: var(--accent-ink);
}

.btn--primary:hover {
  background: var(--accent-strong);
  color: var(--accent-ink);
}

.btn--ghost {
  border-color: var(--line-strong);
  color: var(--text);
  background: transparent;
}

.btn--ghost:hover {
  background: var(--surface-2);
  color: var(--text-strong);
}

.btn--danger {
  border-color: var(--crit);
  color: var(--crit);
  background: transparent;
}

.btn--danger:hover {
  background: var(--crit-wash);
  color: var(--crit);
}

.btn--sm {
  padding: 0.4rem 0.7rem;
  font-size: 0.82rem;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.65rem;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.chip::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.chip--ok {
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 35%, transparent);
  background: var(--ok-wash);
}

.chip--active {
  color: var(--accent);
  border-color: var(--accent-line);
  background: var(--accent-wash);
}

.chip--warn {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 35%, transparent);
  background: var(--warn-wash);
}

.chip--muted {
  color: var(--dim);
}

.chip--plain::before {
  display: none;
}

.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.tag {
  font-family: var(--font-mono);
  font-size: 0.74rem;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.15rem 0.45rem;
  background: var(--surface-2);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   6. Hero
   -------------------------------------------------------------------------- */

.hero {
  padding-block: clamp(3.5rem, 9vw, 7rem) var(--step);
  position: relative;
  overflow: hidden;
}

/* A faint blueprint grid, drawn in CSS so it costs no request. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(var(--line-soft) 1px, transparent 1px),
    linear-gradient(90deg, var(--line-soft) 1px, transparent 1px);
  background-size: 64px 64px;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 20%, transparent 75%);
  opacity: 0.55;
  pointer-events: none;
}

.hero > * {
  position: relative;
}

.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
}

.hero h1 {
  font-size: clamp(2.1rem, 5.4vw, 3.6rem);
  margin-bottom: 1.25rem;
}

.hero__lead {
  color: var(--muted);
  font-size: clamp(1.02rem, 1.6vw, 1.15rem);
  max-width: 68ch;
  margin-bottom: 2rem;
}

.hero__role {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: 1.5rem;
  font-size: 0.92rem;
  color: var(--muted);
}

.hero__role strong {
  color: var(--text);
  font-weight: 600;
}

.identity {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding-bottom: 1.5rem;
  margin-bottom: 1.5rem;
  border-bottom: 1px solid var(--line);
}

.identity img {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--line-strong);
  flex: none;
}

.identity__name {
  font-weight: 600;
  color: var(--text-strong);
}

.identity__meta {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--dim);
}

/* --------------------------------------------------------------------------
   7. Cards and grids
   -------------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
}

.card--pad-lg {
  padding: clamp(1.5rem, 3vw, 2.25rem);
}

.grid {
  display: grid;
  gap: 1rem;
}

.grid--2 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
}

.grid--3 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
}

/* Project card ---------------------------------------------------------- */

.project-card {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding: 1.5rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  text-decoration: none;
  color: inherit;
  position: relative;
  transition: border-color 0.16s ease, background-color 0.16s ease, transform 0.16s ease;
}

.project-card:hover {
  border-color: var(--accent-line);
  background: var(--surface-2);
  color: inherit;
  transform: translateY(-2px);
}

.project-card:hover .project-card__name {
  color: var(--accent);
}

.project-card__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.project-card__name {
  font-size: 1.08rem;
  font-weight: 650;
  color: var(--text-strong);
  letter-spacing: -0.01em;
  transition: color 0.16s ease;
}

.project-card__tagline {
  color: var(--muted);
  font-size: 0.92rem;
  flex: 1;
}

.project-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--line-soft);
  font-family: var(--font-mono);
  font-size: 0.74rem;
  color: var(--dim);
}

.project-card__arrow {
  color: var(--accent);
  font-size: 0.9rem;
}

/* Stat strip ------------------------------------------------------------- */

.stats {
  display: grid;
  /* The container is full-bleed, so auto-fit alone spreads a handful of cells
     across the whole viewport and every label wraps at a different word. A
     wider track keeps them on one line on a desktop and still collapses to a
     single column on a phone. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
}

.stat {
  padding: 1.25rem 1.4rem;
  border-right: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
  /* Labels run to one or two lines depending on the word. Without a floor the
     grid rows size to the tallest and the short cells sit in a pool of space;
     with one, the ragged edge lands inside the cell instead of between them. */
  min-height: 5.6rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.stats__foot {
  /* Spans whatever the column count resolved to. */
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.75rem 1.4rem;
  font-size: 0.82rem;
  color: var(--muted);
  background: var(--bg-sunken);
}

/* The same line, used outside the stats card. */
.stats__foot--bare {
  margin-top: 1rem;
  padding-inline: 0;
  background: none;
}

.stats__dot {
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  background: var(--ok);
  flex: 0 0 auto;
  align-self: center;
}

.stats__foot a {
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--line);
}

.stats__foot a:hover,
.stats__foot a:focus-visible {
  color: var(--text-strong);
  text-decoration-color: currentColor;
}

.stat__value {
  font-size: 1.6rem;
  font-weight: 650;
  color: var(--text-strong);
  letter-spacing: -0.02em;
  font-feature-settings: "tnum";
}

.stat__label {
  font-size: 0.82rem;
  color: var(--muted);
  margin-top: 0.15rem;
}

/* Definition / spec table ------------------------------------------------ */

.spec {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.spec__row {
  display: grid;
  grid-template-columns: minmax(120px, 34%) 1fr;
  gap: 1rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--line-soft);
  font-size: 0.9rem;
}

.spec__row:last-child {
  border-bottom: 0;
}

.spec__row:nth-child(odd) {
  background: var(--surface-2);
}

.spec__key {
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.02em;
}

.spec__value {
  color: var(--text);
}

/* The key column is muted by design, so a link in it needs the underline to
   read as one — colour alone would be too quiet against the label styling. */
.spec__key a {
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px solid var(--line);
}

.spec__key a:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

@media (max-width: 560px) {
  .spec__row {
    grid-template-columns: 1fr;
    gap: 0.2rem;
  }
}

/* Ownership / credential list -------------------------------------------- */

.entry-list {
  border-top: 1px solid var(--line);
}

/* Fixed rail rather than a percentage: at 30% a short title like "Monitoring"
   left a third of the row empty while the detail beside it wrapped early. */
.entry {
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  padding: 1.5rem 0;
  border-bottom: 1px solid var(--line-soft);
}

.entry__title {
  font-weight: 600;
  color: var(--text-strong);
  font-size: 0.98rem;
}

.entry__detail {
  color: var(--muted);
  font-size: 0.95rem;
}

.entry__title a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--line);
}

.entry__title a:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

@media (max-width: 720px) {
  .entry {
    grid-template-columns: 1fr;
    gap: 0.4rem;
  }
}

/* Security scope ----------------------------------------------------------
   The coverage chip and advisory prefix sit under the name in the rail, and
   the advisory link under the note. Both need space above rather than below,
   which is the one thing .eyebrow--tight cannot give them. */

.scope-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.5rem;
}

.scope-link {
  margin-top: 0.6rem;
  font-size: 0.88rem;
}

.scope-note {
  margin-top: 1rem;
  color: var(--muted);
  font-size: 0.94rem;
}

/* Skill groups ------------------------------------------------------------ */

.skill-group {
  padding: 1.25rem 0;
  border-bottom: 1px solid var(--line-soft);
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: baseline;
}

.skill-group__name {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--dim);
}

@media (max-width: 720px) {
  .skill-group {
    grid-template-columns: 1fr;
    gap: 0.7rem;
  }
}

/* --------------------------------------------------------------------------
   8. Long-form prose (Markdown output)
   -------------------------------------------------------------------------- */

.rich {
  max-width: var(--prose);
  color: var(--text);
}

.rich > * + * {
  margin-top: 1.1rem;
}

.rich h2 {
  font-size: 1.35rem;
  margin-top: 2.6rem;
  padding-top: 1.6rem;
  border-top: 1px solid var(--line-soft);
}

.rich h3 {
  font-size: 1.08rem;
  margin-top: 2rem;
}

.rich h4 {
  font-size: 0.96rem;
  margin-top: 1.6rem;
  color: var(--muted);
}

.rich p,
.rich li {
  color: var(--text);
}

.rich ul,
.rich ol {
  padding-left: 1.2rem;
  margin-top: 1rem;
}

.rich li + li {
  margin-top: 0.45rem;
}

.rich li::marker {
  color: var(--dim);
}

.rich blockquote {
  margin: 1.6rem 0;
  padding: 0.2rem 0 0.2rem 1.2rem;
  border-left: 2px solid var(--accent-line);
  color: var(--muted);
}

.rich code {
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 0.08em 0.36em;
  color: var(--text);
}

.rich pre.code {
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem;
  overflow-x: auto;
  font-size: 0.84rem;
  line-height: 1.6;
}

.rich pre.code code {
  background: none;
  border: 0;
  padding: 0;
}

/* --------------------------------------------------------------------------
   9. Page furniture
   -------------------------------------------------------------------------- */

.page-head {
  padding-block: clamp(2.75rem, 6vw, 4.5rem) 0;
}

.page-head h1 {
  font-size: clamp(1.9rem, 4.4vw, 3rem);
}

/* Title and intro sit side by side so the head fills the container instead of
   running the title across the top and the intro halfway across underneath. */
.page-head__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: end;
}

.page-head__intro {
  color: var(--muted);
  font-size: 1.05rem;
  max-width: var(--prose);
  margin-top: 1.1rem;
}

.page-head__grid .page-head__intro {
  max-width: none;
  margin-top: 0;
  padding-bottom: 0.4rem;
}

@media (max-width: 900px) {
  .page-head__grid {
    grid-template-columns: 1fr;
    gap: 1.1rem;
  }

  .page-head__grid .page-head__intro {
    padding-bottom: 0;
  }
}

.breadcrumb {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--dim);
  margin-bottom: 1.25rem;
}

.breadcrumb a {
  color: var(--dim);
  text-decoration: none;
}

.breadcrumb a:hover {
  color: var(--accent);
}

.pager {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 1rem;
  margin-top: var(--step);
}

.pager__item {
  padding: 1.1rem 1.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  text-decoration: none;
  color: inherit;
  background: var(--surface);
}

.pager__item:hover {
  border-color: var(--accent-line);
  color: inherit;
}

.pager__dir {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

.pager__name {
  font-weight: 600;
  color: var(--text-strong);
  margin-top: 0.2rem;
}

.pager__item--next {
  text-align: right;
}

.callout {
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 1.1rem 1.25rem;
  color: var(--muted);
  font-size: 0.94rem;
}

.callout strong {
  color: var(--text);
}

/* Contact ----------------------------------------------------------------- */

.contact-card {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 1.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.contact-card--primary {
  border-color: var(--accent-line);
  background: linear-gradient(180deg, var(--accent-wash), transparent 60%), var(--surface);
}

.contact-card__label {
  font-family: var(--font-mono);
  font-size: 0.74rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

.contact-card__value {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text-strong);
  word-break: break-word;
}

.contact-card__value a {
  color: inherit;
  text-decoration: none;
}

.contact-card__value a:hover {
  color: var(--accent);
}

.contact-card__note {
  color: var(--muted);
  font-size: 0.88rem;
  margin-top: 0.25rem;
}

/* FAQ --------------------------------------------------------------------- */

.faq-item {
  border-bottom: 1px solid var(--line-soft);
}

.faq-item summary {
  cursor: pointer;
  padding: 1.1rem 0;
  font-weight: 600;
  color: var(--text-strong);
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.faq-item summary::-webkit-details-marker {
  display: none;
}

.faq-item summary::after {
  content: "+";
  font-family: var(--font-mono);
  color: var(--accent);
  font-size: 1.15rem;
  flex: none;
}

.faq-item[open] summary::after {
  content: "\2212";
}

.faq-item__body {
  padding-bottom: 1.25rem;
  color: var(--muted);
}

/* Error page -------------------------------------------------------------- */

.error-page {
  padding-block: clamp(4rem, 12vw, 8rem);
  text-align: center;
}

.error-page__code {
  font-family: var(--font-mono);
  font-size: 3.5rem;
  color: var(--accent);
  letter-spacing: -0.03em;
}

.error-page p {
  color: var(--muted);
  max-width: 46ch;
  margin: 1rem auto 2rem;
}

.error-page .btn-row {
  justify-content: center;
}

/* --------------------------------------------------------------------------
   10. Footer
   -------------------------------------------------------------------------- */

.site-footer {
  margin-top: auto;
  border-top: 1px solid var(--line);
  background: var(--bg-sunken);
  padding-block: 3rem 2rem;
}

.site-footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(auto-fit, minmax(140px, 1fr));
  gap: 2rem;
  padding-bottom: 2rem;
  border-bottom: 1px solid var(--line-soft);
}

.site-footer h3 {
  font-family: var(--font-mono);
  font-size: 0.74rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 0.9rem;
  font-weight: 500;
}

.site-footer ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-footer li + li {
  margin-top: 0.5rem;
}

.site-footer a {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.9rem;
}

.site-footer a:hover {
  color: var(--accent);
}

.site-footer__blurb {
  color: var(--muted);
  font-size: 0.9rem;
  max-width: 34ch;
  margin-top: 0.75rem;
}

.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.75rem;
  padding-top: 1.5rem;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  color: var(--dim);
}

/* --------------------------------------------------------------------------
   11. Page-specific components

   Everything here exists so no template needs an inline style attribute —
   the Content-Security-Policy is style-src 'self', with no 'unsafe-inline'.
   -------------------------------------------------------------------------- */

.eyebrow--tight {
  margin-bottom: 0.65rem;
}

.section--tight-top {
  padding-top: 3rem;
}

.stats-band {
  margin-bottom: var(--step);
}

.alt-name {
  color: var(--dim);
  font-weight: 400;
}

.project-card .alt-name {
  font-size: 0.85rem;
}

/* Current-role panel on the home page */
.role-title {
  font-weight: 600;
  color: var(--text-strong);
}

.role-org {
  color: var(--muted);
  font-size: 0.92rem;
  margin-top: 0.2rem;
}

.role-actions {
  margin-top: 1.25rem;
}

/* One block per concurrent role in the hero card. */
.current-role + .current-role {
  margin-top: 0.85rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--line-soft);
}

/* Closing call to action */
.cta-panel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}

.cta-panel h2 {
  font-size: 1.35rem;
}

.cta-panel p {
  color: var(--muted);
  margin-top: 0.5rem;
  max-width: 52ch;
}

/* Project detail layout */
.project-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 320px);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
}

.project-layout__aside {
  position: sticky;
  top: 88px;
}

@media (max-width: 900px) {
  .project-layout {
    grid-template-columns: 1fr;
  }

  .project-layout__aside {
    position: static;
  }
}

.lede {
  font-size: 1.08rem;
  color: var(--text);
  max-width: var(--prose);
  margin-bottom: 2rem;
}

.meta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.9rem;
}

.head-actions {
  margin-top: 1.75rem;
}

.side-note {
  color: var(--text);
  font-size: 0.93rem;
  margin-top: 0.5rem;
}

/* Work page */
.role-block + .role-block {
  margin-top: var(--step);
  padding-top: var(--step);
  border-top: 1px solid var(--line);
}

.role-detail {
  margin-top: 2.5rem;
}

.role-header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.role-header h2 {
  font-size: clamp(1.35rem, 2.6vw, 1.85rem);
}

.role-header__org {
  color: var(--muted);
  margin-top: 0.35rem;
}

/* Contact page */
.contact-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: 1rem;
}

.no-channels {
  margin-top: 2rem;
}

/* Sitemap */
.entry__title .cell-mono {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--dim);
  font-weight: 400;
  margin-top: 0.2rem;
}

.entry__detail .chip {
  margin-bottom: 0.5rem;
}

.entry__detail p {
  margin: 0;
}

/* --------------------------------------------------------------------------
   12. Inline admin controls

   Only rendered for a signed-in administrator. Anonymous visitors never
   receive this markup, so none of it affects the public page weight.
   -------------------------------------------------------------------------- */

.admin-bar {
  position: sticky;
  bottom: 0;
  z-index: 60;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  border-top: 1px solid var(--accent-line);
  box-shadow: 0 -8px 24px -18px rgba(0, 0, 0, 0.7);
}

.admin-bar__inner {
  max-width: var(--container);
  margin-inline: auto;
  padding: 0.65rem clamp(1.1rem, 4vw, 2.5rem);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.admin-bar__who {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.admin-bar__who strong {
  color: var(--text-strong);
  font-weight: 600;
}

.admin-bar__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ok);
  flex: none;
}

.admin-bar__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
}

.admin-bar__logout {
  display: inline;
}

/* Per-item edit affordance. The wrapper exists so the edit link is a sibling
   of the card anchor rather than nested inside it — nested <a> is invalid and
   browsers recover from it unpredictably. */
/* Button sizing rule for the whole site:
     - public pages use the default .btn size, with no exceptions
     - the admin overlay (this section) uses .btn--sm, with no exceptions
   Edit affordances are the same .btn component as everything else. The classes
   below only position them; they never restyle the button. */

.inline-edit {
  margin-left: 0.6rem;
  vertical-align: middle;
}

.card-wrap {
  position: relative;
  display: flex;
}

.card-wrap > .project-card {
  flex: 1;
}

.card-edit {
  position: absolute;
  top: 0.55rem;
  right: 0.55rem;
  z-index: 2;
  opacity: 0;
  transition: opacity 0.15s ease;
}

.card-wrap:hover .card-edit,
.card-edit:focus-visible {
  opacity: 1;
}

/* Touch devices have no hover, so keep it permanently visible there. */
@media (hover: none) {
  .card-edit {
    opacity: 1;
  }
}

/* --------------------------------------------------------------------------
   13. Print
   -------------------------------------------------------------------------- */

@media print {
  .site-header,
  .site-footer,
  .pager,
  .btn-row,
  .admin-bar,
  .card-edit {
    display: none !important;
  }

  body {
    background: #fff;
    color: #000;
  }

  .hero::before {
    display: none;
  }
}

/* ---------------------------------------------------------------------------
   Live figures
   
   The site quotes numbers it fetches rather than numbers it remembers, and
   these are the pieces that say so: the date under the proof-point strip, the
   status line on /security/record, and the source citation that sits under
   every value there.
   --------------------------------------------------------------------------- */

.record-status {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 1.1rem 1.3rem;
}

.record-status__line {
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  flex-wrap: wrap;
  color: var(--text-strong);
}

/* Reads as a status indicator without claiming to be one: it is painted from
   the same token whether the figures are two minutes or two hours old, because
   the page has already refused to render this block at all when they are
   stale. A dot that is always green is decoration; this one is only ever
   drawn when it would be. */
.record-status__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--ok, #2e9e5b);
  flex: 0 0 auto;
  align-self: center;
}

.record-status__note {
  margin-top: 0.35rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.metric-key {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.78rem;
  color: var(--dim);
}

.metric-key code {
  font-size: inherit;
}

.metric-value {
  display: block;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
  color: var(--text-strong);
  font-weight: 600;
}

.metric-source {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.78rem;
  color: var(--muted);
  font-weight: 400;
}

/* The repository panel in a project sidebar. Same idea as the record page,
   compressed to the two facts a reader of that page actually wants: how much
   of it is mine, and whether it is still moving. */
.repo-facts {
  display: grid;
  gap: 0.45rem;
}

.repo-facts__row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.88rem;
}

.repo-facts__key {
  color: var(--muted);
}

.repo-facts__value {
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

.repo-facts__note {
  margin-top: 0.55rem;
  font-size: 0.78rem;
  color: var(--dim);
}

/* --------------------------------------------------------------------------
   14. Legal pages

   Impressum, privacy notice, terms. These are reference documents: nobody
   reads one top to bottom, they arrive looking for one clause, usually from a
   footer link, often on a phone, and often slightly annoyed. So they get a
   contents rail, real sections, and an address block that is structured data
   rather than a paragraph which happens to contain a postcode.

   The address in particular: written as Markdown it came out as one run-on
   line, because Markdown collapses single newlines and a postal address is
   nothing but single newlines. It is rendered by the template now.
   -------------------------------------------------------------------------- */

.legal-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.75rem 1.5rem;
  margin-top: 1.5rem;
  font-size: 0.875rem;
  color: var(--dim);
}

.legal-lang {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid var(--accent-line);
}

.legal-lang:hover {
  border-bottom-color: var(--accent);
}

.legal-layout {
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: start;
}

/* One column below the point where a 15rem rail stops being a rail and starts
   being a squeeze. */
@media (max-width: 56rem) {
  .legal-layout {
    grid-template-columns: minmax(0, 1fr);
  }
}

.legal-toc {
  position: sticky;
  top: 88px;
  font-size: 0.875rem;
}

@media (max-width: 56rem) {
  .legal-toc {
    position: static;
    padding-bottom: 1.5rem;
    border-bottom: 1px solid var(--line);
  }
}

.legal-toc ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
  counter-reset: legal;
}

.legal-toc li {
  counter-increment: legal;
  display: grid;
  grid-template-columns: 1.6rem 1fr;
  gap: 0.4rem;
}

/* Numbered, because "section 4" is how somebody refers to one of these in an
   email, and an unnumbered list gives them nothing to say. */
.legal-toc li::before {
  content: counter(legal);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--dim);
  padding-top: 0.15rem;
}

.legal-toc a {
  color: var(--muted);
  text-decoration: none;
  line-height: 1.4;
}

.legal-toc a:hover {
  color: var(--accent);
}

.legal-body .rich > h2:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* A heading jumped to from the contents must not sit flush against the top of
   the viewport under the sticky header. */
.legal-body h2 {
  scroll-margin-top: 96px;
}

.legal-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: clamp(1.25rem, 3vw, 1.85rem);
  margin-bottom: 2.5rem;
}

.legal-card address {
  font-style: normal;
  display: grid;
  gap: 0.15rem;
  line-height: 1.5;
}

.legal-card address strong {
  color: var(--text-strong);
  font-weight: 600;
}

.legal-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.35rem 1rem;
  margin: 1.25rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line-soft);
  font-size: 0.9rem;
}

.legal-facts dt {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--dim);
  padding-top: 0.15rem;
}

.legal-facts dd {
  margin: 0;
  color: var(--text);
}

/* --------------------------------------------------------------------------
   15. Language switch
   -------------------------------------------------------------------------- */

.lang-switch {
  display: flex;
  align-items: center;
  gap: 1px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.lang-switch a {
  padding: 0.28rem 0.5rem;
  border-radius: calc(var(--radius) - 3px);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  color: var(--muted);
  text-decoration: none;
  line-height: 1;
}

.lang-switch a:hover {
  color: var(--text);
  background: var(--surface-3);
}

.lang-switch a[aria-current="true"] {
  color: var(--accent-ink);
  background: var(--accent);
  font-weight: 600;
}

.lang-switch a:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

/* --------------------------------------------------------------------------
   16. The blog
   --------------------------------------------------------------------------

   Two separate jobs live in this section, and the first one is a bug fix.

   ARTICLE BODIES FROM WORDPRESS.

   Section 8 above styles `.rich` for the output of Markdown.php, which emits
   `<pre class="code">`. WordPress emits `<pre class="wp-block-code">`, and
   Html::clean() -- correctly -- strips every class attribute, so what actually
   arrives on the page is a bare `<pre>`. It matched none of those rules: code
   blocks rendered as unwrapped, unscrollable text with every inline `<code>`
   inside them still carrying its own border and background.

   The same gap covered `img`, `figure`, `figcaption`, `table`, `hr` and the
   definition lists: all of them are on the sanitiser's allowlist, all of them
   are things WordPress emits routinely, and none of them had a single rule.
   An article with a picture and a table in it was unstyled in exactly the
   places a reader looks.

   So the rules below are attached to the ELEMENTS rather than to classes. That
   is the only option that can work here, and it is also the right one: the
   sanitiser's allowlist is the contract for what a post body may contain, and
   this is that list styled. Nothing can arrive in a body that this section has
   not accounted for, because Html::clean() would have removed it.
   -------------------------------------------------------------------------- */

/* A picture never exceeds its column, and never distorts to fit it. The
   aspect-ratio comes from the width/height attributes the template prints from
   the stored dimensions, so the space is held before the bytes arrive. */
.rich img {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-2);
}

.rich figure {
  margin: 1.8rem 0;
}

.rich figcaption {
  margin-top: 0.6rem;
  color: var(--muted);
  font-size: 0.86rem;
  line-height: 1.5;
}

/* Bare <pre> is what a WordPress code block becomes once its class is stripped.
   Both spellings get the same treatment: a reader cannot tell which editor a
   post was written in and must not be able to. */
.rich pre,
.rich pre.code {
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem;
  overflow-x: auto;
  font-size: 0.84rem;
  line-height: 1.6;
  font-family: var(--font-mono);
}

/* An inline <code> is a chip. The same element inside a block is not, and
   inheriting the chip treatment there is what made WordPress code blocks look
   broken rather than merely unstyled. */
.rich pre code,
.rich pre.code code {
  background: none;
  border: 0;
  padding: 0;
  font-size: inherit;
}

/* Tables scroll inside themselves rather than widening the article. The
   wrapper the block editor used to emit is gone with its class, so the
   overflow has to live on the table itself. */
.rich table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
  margin: 1.6rem 0;
  display: block;
  overflow-x: auto;
}

.rich th,
.rich td {
  border: 1px solid var(--line);
  padding: 0.5rem 0.7rem;
  text-align: left;
  vertical-align: top;
}

.rich th {
  background: var(--surface-2);
  color: var(--text-strong);
  font-weight: 600;
  white-space: nowrap;
}

.rich caption {
  caption-side: bottom;
  color: var(--muted);
  font-size: 0.85rem;
  padding-top: 0.6rem;
  text-align: left;
}

.rich hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: 2.2rem 0;
}

.rich dl {
  margin: 1.4rem 0;
}

.rich dt {
  color: var(--text-strong);
  font-weight: 600;
  margin-top: 1rem;
}

.rich dd {
  margin: 0.3rem 0 0;
  color: var(--muted);
}

.rich a {
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px solid var(--accent-line);
}

.rich a:hover {
  border-bottom-color: var(--accent);
  color: var(--text-strong);
}

.rich blockquote cite {
  display: block;
  margin-top: 0.5rem;
  font-style: normal;
  font-size: 0.85rem;
  color: var(--dim);
}

.rich :is(h2, h3, h4):first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* Anchored headings: the sanitiser keeps an id on h2-h6, so a link into the
   middle of a long post works. Give it somewhere to land clear of the header. */
.rich :is(h2, h3, h4, h5, h6)[id] {
  scroll-margin-top: 5rem;
}

/* --- The index ---------------------------------------------------------- */

/* The newest post, at a size that lets its opening line do some work. Falls to
   one column below the breakpoint rather than shrinking the picture, which at
   this width would leave a thumbnail with a paragraph wrapped around it. */
.lead-post {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
  padding-bottom: var(--step);
  border-bottom: 1px solid var(--line);
}

.lead-post:only-child,
.lead-post:last-child {
  border-bottom: 0;
}

/* No picture, no split: a lone column of text pinned to half the page reads as
   a layout with something missing. */
.lead-post:not(:has(.lead-post__media)) {
  grid-template-columns: minmax(0, 1fr);
}

.lead-post__media {
  display: block;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--surface-2);
}

.lead-post__media img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.lead-post__title {
  font-size: clamp(1.4rem, 3vw, 1.9rem);
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin-bottom: 0.7rem;
}

.lead-post__title a {
  color: var(--text-strong);
  text-decoration: none;
}

.lead-post__title a:hover {
  color: var(--accent);
}

.lead-post__excerpt {
  color: var(--muted);
  max-width: 46ch;
  margin-bottom: 1rem;
}

@media (max-width: 720px) {
  .lead-post {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Year headings. A rule with the year sitting on it, so the list reads as one
   continuous thing that happens to be dated rather than as separate blocks. */
.post-year {
  margin-top: var(--step);
}

.post-year__label {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  color: var(--dim);
  margin-bottom: 0.4rem;
}

/* A post row is the site's .entry with a date rail, a body and an optional
   thumbnail. The third column collapses to nothing when there is no picture,
   so one markup shape covers both. */
.entry--post {
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr) auto;
  align-items: start;
}

.entry__when {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
}

.entry__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  margin-top: 0.55rem;
  color: var(--dim);
  font-family: var(--font-mono);
  font-size: 0.74rem;
}

.entry__thumb {
  display: block;
  width: 108px;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--surface-2);
}

.entry__thumb img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

@media (max-width: 720px) {
  .entry--post {
    grid-template-columns: minmax(0, 1fr);
  }

  .entry__when {
    flex-direction: row;
    align-items: center;
  }

  /* The thumbnail is decorative and duplicates the link beside it. On a narrow
     screen it costs a scroll and earns nothing, so it goes. */
  .entry__thumb {
    display: none;
  }
}

/* --- Topics -------------------------------------------------------------- */

.topic-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding-bottom: 0.25rem;
}

/* A tag that is a link, as opposed to the static ones on the about page. */
.tag--link {
  text-decoration: none;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.tag--link:hover {
  border-color: var(--accent-line);
  color: var(--text-strong);
}

.tag__count {
  color: var(--dim);
  margin-left: 0.35rem;
}

.tag--all {
  border-style: dashed;
}

.topic-grid {
  margin-bottom: var(--step);
}

.topic-card {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  text-decoration: none;
  color: inherit;
}

.topic-card:hover {
  border-color: var(--accent-line);
}

.topic-card__name {
  font-size: 1.02rem;
  font-weight: 600;
  color: var(--text-strong);
}

.topic-card__note {
  color: var(--muted);
  font-size: 0.92rem;
}

.topic-card__count {
  margin-top: auto;
  padding-top: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  color: var(--dim);
}

/* --- One post ------------------------------------------------------------ */

.post-hero {
  margin: 0;
}

.post-hero img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 30rem;
  object-fit: cover;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  background: var(--surface-2);
}

.post-hero figcaption {
  margin-top: 0.6rem;
  color: var(--muted);
  font-size: 0.86rem;
}

/* A machine translation says so. Same shape as any other note on the site --
   it is information, not a warning. */
.callout--note {
  margin-bottom: var(--step);
  max-width: var(--prose);
}

.callout--note a {
  color: var(--text);
}

/* The other half of a product, above the write-up.
   Sits between the summary and the body, so it needs air underneath it and
   none above: the lede already provides that. The prose inside is Markdown
   from the database, so the first and last block margins are collapsed rather
   than trusted -- an editor writing two paragraphs here should not be able to
   knock the box out of alignment with the text below it. */
.callout--companion {
  margin-bottom: var(--step);
  max-width: var(--prose);
}

.callout--companion .eyebrow {
  margin-bottom: 0.4rem;
}

.callout--companion .rich > :first-child {
  margin-top: 0;
}

.callout--companion .rich > :last-child {
  margin-bottom: 0;
}

.callout--companion p:last-child {
  margin-bottom: 0;
}

/* A superseded document, said before the text rather than after it.
   Warm rather than red: this is not an error, and a reader who accepted this
   version is looking at the document that governs their agreement. It needs to
   be noticed, not to alarm. */
.callout--superseded {
  border-left-color: var(--warn, #b45309);
  margin-bottom: var(--step);
  max-width: var(--prose);
}

.callout--superseded p:first-child {
  margin-top: 0;
}

.callout--superseded p:last-child {
  margin-bottom: 0;
}

/* The identifier and hash a product's acceptance record points at. Monospace
   and breakable: a 64-character hash must wrap rather than push the page
   sideways on a phone. */
.legal-fingerprint {
  margin-top: var(--step);
}

.legal-fingerprint p {
  margin: 0 0 0.6rem;
}

.legal-fingerprint code.wrap {
  word-break: break-all;
  overflow-wrap: anywhere;
}

.legal-archive__versions {
  list-style: none;
  padding: 0;
  margin: 0.6rem 0 0;
}

.legal-archive__versions li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: baseline;
  padding: 0.3rem 0;
}

.legal-archive__versions code {
  word-break: break-all;
}

.feed-note {
  margin-top: var(--step);
  padding-top: 1rem;
  border-top: 1px solid var(--line-soft);
  font-family: var(--font-mono);
  font-size: 0.76rem;
  color: var(--dim);
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.feed-note a {
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid var(--line);
}

.feed-note a:hover {
  color: var(--text-strong);
  border-bottom-color: var(--accent-line);
}

/* A pinned post's chip carries no status dot: it is a label, not a state. */
.chip--pin::before {
  display: none;
}

/* A preview is a page that looks exactly like the published article, which is
   the point of it and also its one danger: somebody reads it, believes it is
   live, and acts on that. So the bar is the one element on the site allowed to
   shout, and it sits above everything including the header. */
.preview-bar {
  background: var(--warn-wash);
  border-bottom: 1px solid var(--warn);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 0.76rem;
  padding: 0.6rem 0;
}

.preview-bar .container {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.75rem;
}

.preview-bar strong {
  color: var(--warn);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* The password gate. One field and one button, at the width of the field
   rather than the page: a full-width input for a short secret looks like a
   search box and invites the wrong thing to be typed into it. */
.pw-gate {
  max-width: 26rem;
}

.pw-gate__label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.45rem;
}

.pw-gate__row {
  display: flex;
  gap: 0.5rem;
}

.pw-gate__input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

.pw-gate__input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.pw-gate__error {
  margin-bottom: 0.8rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--crit);
  border-radius: var(--radius);
  background: var(--crit-wash);
  color: var(--text);
  font-size: 0.92rem;
}

.pw-gate__note {
  margin-top: 0.8rem;
  font-size: 0.86rem;
  color: var(--dim);
}

/* ---------------------------------------------------------------------------
   The contact form.

   The controls are sized the way admin.css sizes its own -- explicit
   font-family, font-size and line-height, never the `font: inherit`
   shorthand. That shorthand also resets line-height, and a browser's default
   line-height for an input, a select and a textarea are three different
   values, so three controls given identical padding come out three different
   heights. Which is exactly what a row of them makes obvious.

   The select is drawn from scratch for the same reason: a native one ignores
   vertical padding on most platforms and sizes itself, so it will never match
   the input beside it. appearance:none plus an explicit height and a chevron
   painted in the background makes it the same box as everything else.
   --------------------------------------------------------------------------- */

.contact-form {
  /* A form's measure is not prose's. 90ch of input is a very long way for the
     eye to travel back from the end of a line to the start of the next label. */
  max-width: 46rem;
}

.contact-form__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: 1rem;
  /* Top-aligned, so a field carrying an error message does not drag the field
     beside it down half a line. */
  align-items: start;
}

/* Spacing between STACKED blocks only.
   The third selector is scoped with `>` on purpose: the two fields inside a
   .contact-form__row are also adjacent siblings, so an unscoped
   `.contact-form__field + .contact-form__field` pushed the right-hand field of
   every pair 1rem below the left one -- the email input sitting a line lower
   than the name input, in both rows. */
.contact-form__row + .contact-form__row,
.contact-form__row + .contact-form__field,
.contact-form > .contact-form__field + .contact-form__field {
  margin-top: 1rem;
}

/* Label and control as rows of one grid, so every label in a row occupies the
   same height whether or not its text wraps -- which is what keeps the two
   controls beside each other on the same line. */
.contact-form__field {
  display: grid;
  grid-template-rows: auto auto;
  align-content: start;
}

.contact-form__field > label {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  min-height: 1.4rem;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.45rem;
}

.contact-form__optional {
  text-transform: none;
  letter-spacing: 0;
  color: var(--dim);
  font-style: italic;
}

/* One box, three elements. Every value here is stated rather than inherited. */
.contact-form__field input,
.contact-form__field select,
.contact-form__field textarea {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 0.92rem;
  line-height: 1.5;
}

.contact-form__field input,
.contact-form__field select {
  height: 2.65rem;
}

.contact-form__field select {
  /* Drawn, not native: a native select sizes itself and will not match the
     input beside it. The chevron is a gradient pair rather than an image,
     because this site loads no external resources of any kind. */
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.2rem;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position:
    calc(100% - 1.15rem) calc(50% + 1px),
    calc(100% - 0.85rem) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.contact-form__field textarea {
  /* Vertical only. Dragging a textarea wider than its column breaks the grid
     around it, and there is nothing to be gained from a wider one here. */
  resize: vertical;
  min-height: 12rem;
  line-height: 1.6;
}

.contact-form__field input:focus-visible,
.contact-form__field select:focus-visible,
.contact-form__field textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.contact-form__field input[aria-invalid="true"],
.contact-form__field textarea[aria-invalid="true"] {
  border-color: var(--crit);
}

/* The honeypot.

   Clipped rather than display:none or hidden, both of which are the first
   thing a bot worth defending against checks for. The clip pattern takes the
   field out of the layout completely -- an off-screen `left: -9999px` is the
   other common way to do this and can extend the scrollable area sideways,
   which is a real bug for the sake of a hidden input. aria-hidden and
   tabindex="-1" on the markup keep it away from everybody else. */
.contact-form__trap {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.form-error {
  margin: 0.4rem 0 0;
  font-size: 0.86rem;
  line-height: 1.45;
  color: var(--crit);
}

.form-error--summary {
  margin: 0 0 1.2rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--crit);
  border-radius: var(--radius);
  background: var(--crit-wash);
  color: var(--text);
  font-size: 0.92rem;
}

/* The button and the note it sits beside. Top-aligned, not centre-aligned:
   the note runs to two or three lines on a narrow column, and centring floats
   the button against the middle of a paragraph instead of lining it up with
   the first line. */
.contact-form__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  gap: 1rem 1.25rem;
  margin-top: 1.5rem;
}

.contact-form__foot .btn {
  /* Never shrink to fit the note beside it. */
  flex: 0 0 auto;
}

.contact-form__note {
  flex: 1 1 16rem;
  margin: 0;
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--dim);
}

.contact-form__redirect {
  margin: 1.4rem 0 0;
  padding-top: 1.2rem;
  border-top: 1px solid var(--line-soft);
  font-size: 0.88rem;
  color: var(--muted);
}
