/* ==========================================================================
   mcgill.nl — design tokens

   Named for the job each colour does, not for the colour it happens to be.
   The previous set was named --cream, --green, --terracotta and was lifted
   from a farm's editorial palette; when the palette changed, every name in
   the stylesheet became a small lie. These names survive the next change.

   Measured, not asserted. Run `npm run contrast` to re-verify; the numbers below
   are read back out of these tokens, so they cannot drift from what they describe.
   On --paper: --ink 16.75:1, --body 11.82:1, --accent 8.15:1, --muted 5.65:1.
   On --ink: --on-dark 16.75:1, --on-dark-accent 7.97:1, --on-dark-muted 7.06:1.

   Every one clears AA for normal text, so unlike the old --terracotta at
   2.71:1 there is no colour here that has to be kept away from words.
   ========================================================================== */
:root {
  /* Light ground */
  --paper: #f7f8fa;
  --paper-alt: #ffffff;
  --ink: #14181f;
  --body: #2c3440;
  --muted: #5a6472;
  --accent: #1f4e79;
  --rule: #dce0e6;

  /* Ink ground */
  --on-dark: #f7f8fa;
  --on-dark-muted: #9aa4b2;
  --on-dark-accent: #7fb2e5;
  --rule-on-dark: #333b47;

  --font-display: 'Fraunces', Georgia, serif;
  --font-body: 'Nunito', system-ui, sans-serif;
  --font-label: 'Inter', system-ui, sans-serif;

  --container: 72rem;
  /* Fixed, not ch. Every <p> gets this, but ch is font-relative — it is the
     width of a "0" at that element's own size — so a single 65ch produced
     three different boxes: 549px on the 12.8px eyebrow, 655px on body text,
     749px on the 19.2px lede. One rule, three right edges, which reads as a
     ragged column rather than a hierarchy. A rem value gives every text block
     the same right edge and hands the larger type a shorter line, which is
     the correct way round. 34rem lands body text near 71 characters and the
     lede near 64, both inside the 45-75 the eye can track. */
  --measure: 34rem;
  /* Denser than the essay pacing this page used to have. A one-pager is
     scanned, not read through, so sections sit closer together. */
  --section-y: clamp(3rem, 6vw, 5rem);
}

/* Latin-subset variable woff2, self-hosted — the copy is Dutch and needs no
   Cyrillic or Greek. format('woff2') rather than format('woff2-variations'):
   the latter is not a format keyword in the current CSS Fonts spec, and a
   browser that does not recognise it skips the source entirely, silently
   dropping the page onto the Georgia/system fallback. */
@font-face {
  font-family: 'Fraunces';
  src: url('/fonts/fraunces-variable.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Nunito';
  src: url('/fonts/nunito-variable.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-variable.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--body);
  font-family: var(--font-body);
  font-size: 1.05rem;
  line-height: 1.7;
  -webkit-text-size-adjust: 100%;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 500;
  color: var(--ink);
  line-height: 1.08;
  letter-spacing: -0.02em;
  font-variation-settings: 'SOFT' 60, 'WONK' 1, 'opsz' 144;
  margin: 0 0 0.5em;
}
h1 { font-size: clamp(2.5rem, 7vw, 4.75rem); }
h2 { font-size: clamp(1.9rem, 5vw, 3.25rem); }
h3 { font-size: clamp(1.3rem, 3vw, 1.75rem); font-variation-settings: 'SOFT' 80, 'WONK' 1, 'opsz' 36; }

p { max-width: var(--measure); }

/* Every link on the LEAF page was either on the ink ground or wrapped an
   image, so there was never a base colour for a link on paper. The landing
   needs one. --accent measures 8.15:1 on --paper, so it also works as text. */
a { color: var(--accent); text-underline-offset: 4px; }

/* --- Section shell --- */
.section { padding: var(--section-y) 1.5rem; }
.section--light { background: var(--paper); }
.section--light-alt { background: var(--paper-alt); }
.section--dark { background: var(--ink); color: var(--on-dark); }
.section--dark h1, .section--dark h2, .section--dark h3 { color: var(--on-dark); }
.section--dark .eyebrow { color: var(--on-dark-accent); }

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

.eyebrow {
  font-family: var(--font-label);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 1.25rem;
}

.lede { font-size: 1.2rem; }

/* Square source, cropped round. Sized in the attribute rather than left to the
   intrinsic 320 so the box is reserved before the file arrives. */
.avatar {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  display: block;
  margin: 0 0 1.5rem;
  background: var(--rule-on-dark);
}

/* Sub-pages carry one link back to the root. Deliberately not a nav bar: with
   five pages a bar is more chrome than navigation, and the landing already
   lists everything. */
.backlink {
  font-family: var(--font-label);
  font-size: 0.85rem;
  margin: 0 0 2rem;
}
.backlink a { text-underline-offset: 4px; }

/* Decorative only — never carries text (see contrast note above). */
.rule {
  width: 4rem;
  height: 3px;
  background: var(--accent);
  border: 0;
  margin: 2rem 0;
}
/* Keyboard-only scroll-through is part of the accessibility pass, so the ring
   has to stay visible on both grounds. A focus indicator needs 3:1 against its
   surroundings: --ink on --paper gives 16.75:1, --on-dark-accent on --ink gives
   7.97:1. The accent would have cleared it on light too — unlike the palette
   this replaced, nothing here fails — but ink stays the most legible. */

:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
}
.section--dark :focus-visible { outline-color: var(--on-dark-accent); }

/* ==========================================================================
   Section archetypes
   ========================================================================== */

/* --- Stats (§2) --- */
.stats { display: flex; flex-wrap: wrap; gap: 3rem; margin: 3rem 0; padding: 0; }
.stat { margin: 0; display: flex; flex-direction: column; }
.stat dd {
  font-family: var(--font-display);
  font-size: clamp(3rem, 9vw, 5.5rem);
  font-weight: 500;
  line-height: 1;
  color: var(--ink);
  margin: 0;
  font-variation-settings: 'SOFT' 60, 'WONK' 1, 'opsz' 144;
}
.stat dt {
  font-family: var(--font-label);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  order: 2;
  margin-top: 0.5rem;
}
.section--dark .stat dd { color: var(--on-dark); }
.section--dark .stat dt { color: var(--on-dark-muted); }

/* --- Screenshot figure (§3, §7) --- */
.shot { margin: 2.5rem 0; }
/* A figure that supports a paragraph rather than opening a section. It sits on
   the text measure so it lines up with the column it belongs to, instead of
   running the full container width like the lead figure in §3. */
.shot--measure { max-width: var(--measure); }
.shot img {
  width: 100%;
  height: auto;
  display: block;
  border: 1px solid var(--rule);
  background: var(--paper-alt);
}
.shot figcaption {
  font-family: var(--font-label);
  font-size: 0.85rem;
  color: var(--muted);
  margin-top: 0.75rem;
}
.shot a { display: block; }
.shot a:hover img { border-color: var(--accent); }
.section--dark .shot figcaption { color: var(--on-dark-muted); }

/* --- Two- and three-column blocks (§3, §4, §5) --- */
/* Same min() guard as .gallery: 18rem is 288px, which overflows a viewport
   under about 336px. Rare, but silent when it happens. */
.cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: 2.5rem;
  margin: 2.5rem 0;
}
.cols h3 { margin-bottom: 1rem; }
/* A list of links with a rule between each, used for the landing page's
   "elsewhere" block. Same rhythm as .col-list, but the row is the link. */
.linklist { list-style: none; padding: 0; margin: 2.5rem 0 0; max-width: var(--measure); }
.linklist li { border-top: 1px solid var(--rule); padding: 0.9rem 0; }

.col-list { list-style: none; padding: 0; margin: 0; }
.col-list li { border-top: 1px solid var(--rule); padding: 0.7rem 0; font-size: 0.98rem; }

/* ==========================================================================
   Screens: thumbnails and the library
   The screens carried 01–04 while a thumbnail's only job was to point at the
   matching entry in §7. The lightbox opens the screen directly, so there is
   nothing left to cross-reference and the numbering came out.

   PhotoSwipe is themed here rather than in its own stylesheet, so an upgrade
   is a straight file swap under /vendor.
   ========================================================================== */
.pswp {
  --pswp-bg: #1a2e18;              /* --ink, but PhotoSwipe reads it before :root applies */
  --pswp-icon-color: #fff8ee;
  --pswp-icon-color-secondary: #1a2e18;
  --pswp-error-text-color: #fff8ee;
}

/* --- Inline thumbnail (§2, §4) --- */
.thumb {
  position: relative;
  display: block;
  flex: 0 0 auto;
  width: 11rem;
  align-self: start;
  /* The border is the affordance: it is what changes on hover and focus, so
     the link never depends on colour alone to read as clickable. */
  border: 1px solid var(--rule);
  background: var(--paper-alt);
}
.thumb img { display: block; width: 100%; height: auto; }
.thumb:hover { border-color: var(--accent); }

/* Thumbnail beside its text (§4 portalen, §2 pakketlijst). */
.port, .aside { display: flex; flex-wrap: wrap; gap: 1.5rem; }
.port { align-items: start; }
.port > div, .aside p { flex: 1 1 12rem; }
/* The measure applies to the whole thumbnail-plus-caption unit, not to the
   caption alone. The caption sits 200px in from the left, so giving it the
   full measure would push its right edge that far past every other paragraph
   in the section — the exact raggedness the fixed measure is there to stop. */
.aside { margin-top: 2.5rem; align-items: center; max-width: var(--measure); }
.aside p { margin: 0; }

/* --- The library (§7) --- */
/* Three columns at desktop. Twelve screens in twos would be six rows deep;
   in threes it is four, and the library reads as a contact sheet rather than
   a queue. Each still opens full size, so the thumbnail only has to be
   recognisable.

   min(20rem, 100%) rather than a bare 20rem: a bare minimum is a floor the
   track never goes under, so on any viewport narrower than that plus the
   section padding the column keeps its width and pushes straight out of the
   page. auto-fit drops to one column but cannot shrink that column. */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: 2rem;
  margin-top: 2.5rem;
}
.gallery__item {
  margin: 0;
  /* Arriving from a thumbnail should not land the figure flush against the
     viewport edge. */
  scroll-margin-top: 2rem;
}
/* Uniform tiles, whatever shape the capture is. Most screens land near 1.85
   and fill the tile exactly; a portrait one, such as a PDF invoice, is
   letterboxed against the off-white rather than cropped, because cropping a
   screenshot throws away the thing being shown. The full image is one click
   away in any case, so the tile only has to be recognisable, and declaring
   the ratio in CSS keeps the space reserved before the file loads. */
.gallery__item img {
  display: block;
  width: 100%;
  /* height:auto is load-bearing, not tidiness. Without it the height attribute
     is a definite height, both axes are resolved, and aspect-ratio is ignored
     — the tiles silently keep their natural shapes. */
  height: auto;
  aspect-ratio: 1.85;
  object-fit: contain;
  border: 1px solid var(--rule-on-dark);
  background: var(--paper-alt);
}
/* The arrival marker. Terracotta carries no text here, only the rule. */
.gallery__item:target img { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent); }
.gallery__item figcaption {
  font-family: var(--font-label);
  font-size: 0.85rem;
  color: var(--on-dark-muted);
  margin-top: 0.75rem;
}
.gallery__item a { display: block; }
.gallery__item a:hover img { border-color: var(--accent); }

/* --- Call to action (§8) --- */
.callout {
  background: var(--ink);
  color: var(--on-dark);
  padding: 2rem;
  margin: 2.5rem 0 0;
}
.callout p { max-width: none; }
/* An ink panel inside a light section, so three things that .section--dark
   normally handles have to be repeated here: --green is near-invisible on ink,
   links need the amber they get on dark, and the default ink focus ring would
   land ink-on-ink and disappear. Amber measures 9.2:1 on ink. */
.callout .eyebrow { color: var(--on-dark-accent); }
.callout a { color: var(--on-dark-accent); text-underline-offset: 4px; }
.callout :focus-visible { outline-color: var(--on-dark-accent); }

/* --- Archived posts (/archive) --- */
/* These are 2018-2019 blog posts, so unlike the rest of the site they contain
   running prose with headings, lists, inline code and code blocks. Everything
   here is scoped to .prose so it cannot leak into the pages that were designed
   rather than imported. */
.prose { max-width: var(--measure); }
.prose h2 { font-size: clamp(1.4rem, 3vw, 1.9rem); margin-top: 2.5rem; }
.prose ul, .prose ol { padding-left: 1.2rem; margin: 1.5rem 0; }
.prose li { margin-bottom: 0.5rem; }
.prose img {
  display: block;
  width: 100%;
  height: auto;
  margin: 2rem 0;
  border: 1px solid var(--rule);
  background: var(--paper-alt);
}
/* Inline code and bare URLs are single unbreakable tokens — things like
   aspnet:RequestQueueLimitPerSession. On a phone they push the page sideways
   by up to 190px. A <pre> solves this by scrolling; inline runs have to be
   allowed to break mid-token instead. */
.prose code,
.prose a { overflow-wrap: anywhere; }
.prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  background: var(--paper-alt);
  border: 1px solid var(--rule);
  padding: 0.1em 0.35em;
}
/* Block samples are <pre><code>. They have to scroll inside their own box
   rather than widening the page — the same rule the galleries follow. The
   inner <code> drops the inline treatment, or every block gets a border and a
   background twice over. */
.prose pre {
  max-width: var(--measure);
  overflow-x: auto;
  margin: 1.5rem 0;
  padding: 1rem 1.1rem;
  background: var(--paper-alt);
  border: 1px solid var(--rule);
  font-size: 0.82rem;
  line-height: 1.55;
}
.prose pre code {
  background: none;
  border: 0;
  padding: 0;
  font-size: inherit;
  /* the <pre> scrolls, so its contents must not be broken to fit */
  overflow-wrap: normal;
}
.linklist__meta {
  font-family: var(--font-label);
  font-size: 0.8rem;
  color: var(--muted);
  margin-left: 0.6rem;
}

/* --- Curriculum vitae (/cv) --- */
/* One long light section rather than alternating grounds: a CV is a list, and
   banding every role would read as decoration on something that wants to be
   scanned. The rule between entries does the separating instead. */
.cv-item {
  border-top: 1px solid var(--rule);
  padding-top: 2rem;
  margin-top: 2.5rem;
}
.cv-item h3 { margin-bottom: 0.25rem; }
.cv-meta {
  font-family: var(--font-label);
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0 0 1rem;
}
.cv-meta strong { color: var(--body); font-weight: 600; }
.cv-item .lede { font-size: 1.1rem; }
/* .col-list has no width of its own because on /leaf it always sat inside a
   .cols track, which supplied one. Here it is loose in a full-width container,
   so without this the bullets run to 1152px while every paragraph beside them
   stops at the measure — one block, two right edges. */
.cv-item .col-list { max-width: var(--measure); }

.cv-sub { margin-top: 1.75rem; }
.cv-sub h4 {
  font-family: var(--font-label);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 0.5rem;
}

/* --- Numbered steps (§6) --- */
.steps { list-style: none; counter-reset: step; padding: 0; margin: 2.5rem 0 0; }
.steps li { counter-increment: step; border-top: 1px solid var(--rule); padding: 1.5rem 0; display: grid; grid-template-columns: 4rem 1fr; gap: 1.5rem; }
/* A large display numeral, which is the one place terracotta is allowed to
   carry a glyph. The step name lives in the h3, so nothing depends on it. */
.steps li::before {
  content: counter(step, decimal-leading-zero);
  font-family: var(--font-display);
  font-size: 2rem;
  color: var(--accent);
  line-height: 1;
}
.steps h3 { margin-bottom: 0.5rem; }
.steps p { margin: 0; }

/* --- Footer --- */
.section--dark a { color: var(--on-dark-accent); text-underline-offset: 4px; }
.fineprint { font-size: 0.85rem; color: var(--on-dark-muted); }

/* ==========================================================================
   Scroll reveals
   Scoped to .js, which reveal.js puts on <html>. Without that script — blocked,
   failed, or never loaded — none of this applies and every section is simply
   visible. Content must never depend on a script having run.
   ========================================================================== */
.js .reveal { opacity: 0; transform: translate3d(0, 16px, 0); transition: opacity 0.6s ease, transform 0.6s ease; }
.js .reveal.is-visible { opacity: 1; transform: none; }

/* Following a thumbnail down to §7 is a jump across most of the page, so it
   scrolls rather than teleports — but only where motion is welcome. Declared
   inside a no-preference query rather than being switched off in the reduce
   block, so the default for an unknown preference is no motion. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

@media (prefers-reduced-motion: reduce) {
  .js .reveal { opacity: 1; transform: none; transition: none; }
}

/* Download link with its inline icon. */
.download {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--font-label);
  font-size: 0.9rem;
  text-decoration: none;
  border: 1px solid currentColor;
  padding: 0.55rem 0.95rem;
  margin-top: 0.5rem;
}
.download:hover { text-decoration: underline; }
/* Optical: the glyph sits high against the cap height of the label. */
.dl-icon { flex: none; transform: translateY(1px); }
.download__meta {
  font-size: 0.78rem;
  opacity: 0.7;
  white-space: nowrap;
}
.section--dark .download { color: var(--on-dark-accent); }
