/* ============================================================
   Aidi Ahmi — personal site
   Design tokens first; edit these to restyle the whole site.
   ============================================================ */

:root {
  --bg: #0a0616;
  --fg: #ece9f5;
  --fg-2: rgba(236, 233, 245, .66);
  --fg-3: rgba(236, 233, 245, .50);
  --panel: linear-gradient(160deg, rgba(255,255,255,.075), rgba(255,255,255,.025));
  --brd: rgba(255, 255, 255, .11);
  --brd-2: rgba(255, 255, 255, .06);
  /* Violet into pink. Green went back to meaning only what it means in the
     data — Q1, open access, free APC, ABDC A*, upgraded — which is the one
     hue this site cannot spend on branding without flattening a signal. */
  --accent: #a78bfa;
  --accent-2: #ec4899;
  --accent-3: #38bdf8;
  --glow: 0 24px 70px -30px rgba(124, 58, 237, .85);
  /* What goes ON the accent. Violet-400 is light: white on it is 2.7:1, so the
     primary button needs dark text or it is unreadable. */
  --on-accent: #1a0b2e;

  /* The display gradient — hero headline, aurora, decorative fills. Separate
     from --accent because --accent is also the link colour and has to clear
     4.5:1 against the ground; a 60px headline only needs 3:1, so these get to
     run the whole way from blue through pink to orange. */
  --g1: #60a5fa;
  --g2: #ec4899;
  --g3: #fb923c;
  --aurora: .62;
  --wrap: 1240px;
  --pad: 28px;
  --radius: 20px;
}

html[data-theme="light"] {
  --bg: #faf7fd;
  --fg: #15102a;
  --fg-2: rgba(21, 16, 42, .68);
  --fg-3: rgba(21, 16, 42, .59);
  --panel: linear-gradient(160deg, rgba(255,255,255,.92), rgba(255,255,255,.6));
  --brd: rgba(21, 16, 42, .12);
  --brd-2: rgba(21, 16, 42, .07);
  /* A shade darker than the dark theme's pair throughout: these carry white
     text on the primary button *and* sit on a near-white ground, so they have
     to clear 4.5:1 in both directions. */
  --accent: #6d28d9;
  --accent-2: #be185d;
  --accent-3: #0369a1;
  --g1: #2563eb;
  --g2: #db2777;
  --g3: #ea580c;
  --aurora: .72;
  --on-accent: #ffffff;
  --glow: 0 24px 60px -34px rgba(109, 40, 217, .45);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: "Plus Jakarta Sans", system-ui, sans-serif;
  text-wrap: pretty;
  transition: background .5s ease, color .5s ease;
  overflow-x: hidden;
}

a { color: var(--accent); text-decoration: none; transition: color .2s ease; }
/* A link sitting inside a block of running text has to be distinguishable by
   something other than colour — WCAG 1.4.1, and axe flags it as
   `link-in-text-block`. Buttons, cards, nav items and link grids are not
   running text and keep the clean underline-free treatment; a link inside a
   paragraph gets an underline. The offset keeps descenders legible. */
p > a, li > a:not(.btn), dd > a, .prose a:not(.btn), .note > a, .jprov-note > a {
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
}
p > a:hover, li > a:not(.btn):hover, dd > a:hover, .prose a:not(.btn):hover,
.note > a:hover, .jprov-note > a:hover { text-decoration-color: currentColor; }
/* Except where the "paragraph" is really a row of controls or a card body. */
/* :not(.btn) on each so these out-specify the running-text rule above rather
   than merely following it — equal specificity resolved by source order is
   too easy to break by moving a block. */
.rail-group li > a:not(.btn), .link-grid a:not(.btn), .stack a:not(.btn),
.social-icons a:not(.btn), .dsrc-lists li > a:not(.btn), .pub-links a:not(.btn),
.ncard-links a:not(.btn), .vlist a:not(.btn), .nhero-links a:not(.btn),
.cv-pubs a:not(.btn), .note-card a:not(.btn), .jactions a:not(.x) {
  text-decoration: none;
}
a:hover { color: var(--accent-2); }
::selection { background: color-mix(in srgb, var(--accent) 35%, transparent); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
code { font-family: "JetBrains Mono", monospace; font-size: .9em; color: var(--fg-2); }

/* ── background decoration ────────────────────────────────── */
.bg-aurora {
  position: fixed; inset: -10% -5%; pointer-events: none; z-index: 0;
  /* green on the left, blue on the right, the way the headline runs. The
     stops are the display triple, not --accent: this is the largest coloured
     surface on the page and the muted link green made it look like fog. */
  background:
    radial-gradient(48% 52% at 28% 26%, color-mix(in srgb, var(--g1) 62%, transparent), transparent 72%),
    radial-gradient(46% 62% at 90% 40%, color-mix(in srgb, var(--g3) 55%, transparent), transparent 72%),
    radial-gradient(46% 46% at 60% 86%, color-mix(in srgb, var(--g2) 42%, transparent), transparent 70%);
  filter: blur(30px); opacity: var(--aurora);
  animation: drift 22s ease-in-out infinite;
}
.bg-grid {
  position: fixed; inset: 0; pointer-events: none; z-index: 0;
  background-image: linear-gradient(var(--brd-2) 1px, transparent 1px), linear-gradient(90deg, var(--brd-2) 1px, transparent 1px);
  background-size: 72px 72px;
  mask-image: radial-gradient(70% 60% at 50% 0%, #000 20%, transparent 85%);
}

/* ── header ───────────────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: 20;
  backdrop-filter: blur(20px);
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  border-bottom: 1px solid var(--brd-2);
}
.nav-inner {
  max-width: var(--wrap); margin: 0 auto; padding: 16px var(--pad);
  display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 10px; color: var(--fg); font-weight: 700; font-size: 16px; letter-spacing: -.01em; }
/* ── brand mark ─────────────────────────────────────────────
   The artwork is a raster tile that carries its own rounded corners
   in its alpha channel, so it gets no border-radius and no box-shadow —
   a box-shadow would draw a rectangle around the squircle. Depth comes
   from drop-shadow(), which follows the PNG's alpha instead.
   ──────────────────────────────────────────────────────────── */
.brand-badge {
  position: relative; display: inline-grid; place-items: center;
  width: 32px; height: 32px; flex: 0 0 auto;
  perspective: 260px;
}
.brand-mark {
  width: 32px; height: 32px; display: block; border-radius: 0;
  filter: drop-shadow(0 2px 7px rgba(139, 92, 246, .40));
  transition: transform .45s cubic-bezier(.22, 1, .36, 1),
              filter   .45s ease;
  will-change: transform;
}
/* a soft bloom behind the tile, revealed on hover */
.brand-badge::after {
  content: ""; position: absolute; inset: -32%;
  border-radius: 50%; pointer-events: none; z-index: -1;
  background: radial-gradient(circle,
    rgba(155, 106, 250, .55), rgba(236, 72, 153, .18) 46%, transparent 72%);
  opacity: 0; transform: scale(.6);
  transition: opacity .45s ease, transform .55s cubic-bezier(.22, 1, .36, 1);
}

.brand:hover .brand-mark,
.brand:focus-visible .brand-mark {
  transform: translateY(-1px) scale(1.12) rotate(-4deg);
  filter: drop-shadow(0 6px 16px rgba(139, 92, 246, .72))
          saturate(1.12) brightness(1.06);
  animation: brandTilt 2.6s ease-in-out .45s infinite;
}
.brand:hover .brand-badge::after,
.brand:focus-visible .brand-badge::after { opacity: 1; transform: scale(1); }

/* after the initial lift, drift gently rather than sitting still */
@keyframes brandTilt {
  0%, 100% { transform: translateY(-1px) scale(1.12) rotate(-4deg); }
  50%      { transform: translateY(-3px) scale(1.14) rotate(3deg); }
}

@media (prefers-reduced-motion: reduce) {
  .brand-mark { transition: none; }
  .brand:hover .brand-mark, .brand:focus-visible .brand-mark {
    animation: none; transform: scale(1.04);
  }
  .brand-badge::after { transition: none; }
}
.nav-links { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 14px; }
.nav-links a[data-route] { color: var(--fg-2); padding: 8px 14px; border-radius: 10px; }
.nav-links a[data-route]:hover { color: var(--fg); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.nav-links a[data-route].is-active { color: var(--fg); background: color-mix(in srgb, var(--accent) 16%, transparent); }
.icon-btn {
  width: 38px; height: 38px; margin: 0 4px; display: grid; place-items: center;
  border-radius: 12px; border: 1px solid var(--brd); background: var(--panel);
  color: var(--fg); font-size: 15px; cursor: pointer;
}
.icon-btn:hover { border-color: var(--accent); }

/* ── buttons ──────────────────────────────────────────────── */
.btn { display: inline-block; font-size: 15px; font-weight: 600; padding: 13px 24px; border-radius: 14px; }
.btn-primary { color: var(--on-accent); background: linear-gradient(100deg, var(--accent), var(--accent-2)); box-shadow: var(--glow); }
.btn-primary:hover { color: var(--on-accent); filter: brightness(1.08); }
.btn-ghost { color: var(--fg); border: 1px solid var(--brd); background: var(--panel); backdrop-filter: blur(12px); }
.btn-ghost:hover { color: var(--fg); border-color: var(--accent); }
.nav-links .btn { padding: 10px 18px; font-size: 14px; }

/* ── layout ───────────────────────────────────────────────── */
.page { position: relative; z-index: 1; max-width: var(--wrap); margin: 0 auto; padding: 0 var(--pad); }
.page > section { padding: 0 0 clamp(40px, 4.4vw, 60px); }
.page > section:first-child { padding-top: clamp(34px, 4vw, 56px); }
.page[hidden] { display: none; }

h1, h2, h3 { letter-spacing: -.02em; }
.page-title { font-size: clamp(32px, 4.2vw, 54px); font-weight: 800; line-height: 1.06; margin: 16px 0 0; max-width: 26ch; }
.page > section > h2 { font-size: clamp(26px, 3vw, 38px); font-weight: 700; margin: 0 0 14px; }
.lead { font-size: 16.5px; line-height: 1.72; max-width: 62ch; margin: 20px 0 0; color: var(--fg-2); }
.sub { font-size: 15.5px; margin: 0 0 24px; color: var(--fg-2); max-width: 58ch; }
.note { font-size: 13px; margin: 12px 0 0; color: var(--fg-3); }
.kicker { display: block; font-size: 12px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--accent); margin: 0 0 12px; }
.kicker-2 { color: var(--accent-2); margin-top: 26px; }
.kicker-gap { margin-top: 24px; }
.row { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 30px; }
.section-head { display: flex; align-items: end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin: 0 0 22px; }
.section-head h2 { font-size: clamp(26px, 3vw, 38px); font-weight: 700; margin: 12px 0 0; }
.section-head a { font-size: 14.5px; font-weight: 600; }

/* ── hero ─────────────────────────────────────────────────── */
.hero { display: grid; grid-template-columns: minmax(0, 5.9fr) minmax(0, 6.1fr); gap: 48px clamp(20px, 3vw, 44px); align-items: center; }
.hero h1 { font-size: clamp(38px, 5.2vw, 68px); line-height: 1.04; font-weight: 800; letter-spacing: -.03em; margin: 26px 0 0; }
.grad { background: linear-gradient(100deg, var(--g1) 2%, var(--g2) 50%, var(--g3)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.pill {
  display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--accent);
  padding: 8px 14px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-2); animation: pulse 2.4s ease-in-out infinite; }
/* ── hero stage: the researcher inside the data ──────────────
   Layers, back to front:
     1  .portrait-glow      atmospheric light
     2  #hero-net-back      network edges + most nodes
     3  .portrait-media     the photograph, edges masked away
     4  #hero-net-front     a few nodes over the lower torso only
     5  .hero-labels        research annotations and micro-metrics
   The face is a protected zone: app.js keeps nodes and labels out
   of it, and the back layer is covered by the opaque photo anyway.
   ──────────────────────────────────────────────────────────── */
.portrait {
  position: relative; margin: 0;
  /* the network is allowed to breathe past the column edge */
  overflow: visible;
  --par-x: 0px; --par-y: 0px;      /* pointer parallax, set by app.js */
}

.portrait-glow {
  position: absolute; inset: -26% -20% -14%;
  pointer-events: none;
  /* No filter: blur() here. Combined with the scale() in heroBreathe it
     gets promoted to a composited layer and the blur is clipped at that
     layer's edge, which showed as a vertical seam beside the portrait.
     A radial gradient is already smooth, so the blur was redundant. */
  background:
    radial-gradient(closest-side circle at 52% 38%,
      color-mix(in srgb, var(--accent-2) 40%, transparent) 0%,
      color-mix(in srgb, var(--accent) 27%, transparent) 34%,
      color-mix(in srgb, var(--accent-3) 13%, transparent) 60%,
      color-mix(in srgb, var(--accent) 4%, transparent) 80%,
      transparent 100%);
  opacity: .9;
  transform: translate3d(calc(var(--par-x) * .3), calc(var(--par-y) * .3), 0);
  transition: opacity .6s ease;
  animation: heroBreathe 15s ease-in-out infinite;
}
.portrait:hover .portrait-glow { opacity: 1; }

@keyframes heroBreathe {
  0%, 100% { opacity: .82; }
  50%      { opacity: .98; }
}

/* both SVG layers sit over the whole stage and never take clicks */
.hero-net {
  position: absolute; inset: -8% -10%; width: 120%; height: 116%;
  pointer-events: none; overflow: visible;
}
.hero-net.is-back  { z-index: 1; transform: translate3d(calc(var(--par-x) * .55), calc(var(--par-y) * .55), 0); }
.hero-net.is-front { z-index: 3; transform: translate3d(calc(var(--par-x) * 1.15), calc(var(--par-y) * 1.15), 0); }

.hero-edge {
  stroke: color-mix(in srgb, var(--accent) 52%, transparent);
  stroke-width: .8; fill: none; vector-effect: non-scaling-stroke;
}
.hero-edge.is-dotted { stroke-dasharray: 3 7; }
.hero-net { --line-boost: 1; }
html[data-theme="light"] .hero-edge { stroke: color-mix(in srgb, var(--accent) 46%, transparent); }
.hero-node { fill: color-mix(in srgb, var(--accent) 78%, transparent); }
.hero-node.is-ring { fill: none; stroke: color-mix(in srgb, var(--accent-2) 78%, transparent); stroke-width: 1.2; vector-effect: non-scaling-stroke; }
.hero-node.is-key  { fill: var(--accent-2); }
.hero-node.is-dust { fill: color-mix(in srgb, var(--accent) 50%, transparent); }
.hero-spark { fill: var(--accent-2); }

/* a lit node and its edges, on label hover */
.hero-net .is-lit    { stroke: var(--accent-2); stroke-width: 1.6; }
.hero-net .node-lit  { fill: var(--accent-2); }

/* ── the photograph ─────────────────────────────────────────
   No card, no border, no background. The image is masked so its
   rectangle dissolves: soft left/right edges and a long fade at
   the bottom, so the torso sinks into the network. Works with any
   ordinary JPG — no cut-out needed. */
.portrait-media {
  position: relative; z-index: 2;
  /* the figure is the anchor of the hero, so it runs wider than its
     column and slightly taller than a 4:5 box */
  width: 108%; max-width: none; margin: 0 -6% 0 auto; aspect-ratio: 4 / 4.7;
  transform: translate3d(calc(var(--par-x) * .85), calc(var(--par-y) * .85), 0);
  /* The mask lives on the container, not the image, so the blend
     overlay below is dissolved by the same shape. An organic vignette
     removes the photo's rectangle; the bottom fade sinks the torso
     into the network. No straight edge of the original frame survives. */
  -webkit-mask-image:
    radial-gradient(ellipse 50% 42% at 50% 42%, #000 38%, rgba(0,0,0,.92) 58%,
                    rgba(0,0,0,.55) 76%, rgba(0,0,0,.18) 90%, transparent 100%),
    linear-gradient(to bottom, #000 0%, #000 54%, rgba(0,0,0,.7) 73%,
                    rgba(0,0,0,.28) 88%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    radial-gradient(ellipse 50% 42% at 50% 42%, #000 38%, rgba(0,0,0,.92) 58%,
                    rgba(0,0,0,.55) 76%, rgba(0,0,0,.18) 90%, transparent 100%),
    linear-gradient(to bottom, #000 0%, #000 54%, rgba(0,0,0,.7) 73%,
                    rgba(0,0,0,.28) 88%, transparent 100%);
  mask-composite: intersect;
}
.portrait-media img {
  width: 100%; height: 100%; display: block;
  object-fit: cover; object-position: 50% 6%;
}
/* Pulls the outer photo toward the page colour so a studio backdrop
   melts into the hero instead of reading as a pale slab. Masked along
   with the image, so it never draws a box of its own. */
.portrait-media::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse 50% 42% at 50% 42%,
    transparent 34%,
    color-mix(in srgb, var(--bg) 50%, transparent) 62%,
    color-mix(in srgb, var(--bg) 88%, transparent) 84%,
    var(--bg) 100%);
}
html[data-theme="light"] .portrait-media img { filter: contrast(1.02) saturate(1.03); }

/* ── annotations ────────────────────────────────────────────
   Positioned in percentages by app.js, which keeps them outside
   the face exclusion zone. */
.hero-labels {
  /* bleeds left, toward the headline, but never right — a right bleed
     pushes the outer labels past the viewport edge */
  position: absolute; inset: -6% 0 -6% -14%; z-index: 4; pointer-events: none;
  transform: translate3d(calc(var(--par-x) * 1.5), calc(var(--par-y) * 1.5), 0);
}
.hero-label {
  position: absolute; display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px 5px 8px; border-radius: 999px; white-space: nowrap;
  font-size: 11.5px; font-weight: 600; letter-spacing: .01em;
  color: var(--fg-2);
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  box-shadow: 0 3px 14px color-mix(in srgb, var(--accent) 14%, transparent);
  backdrop-filter: blur(8px);
  pointer-events: auto; cursor: default;
  transform: translate(-50%, -50%);
  animation: heroFloat 12s ease-in-out infinite;
  transition: color .35s ease, border-color .35s ease, background .35s ease;
}
.hero-label::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent-2); flex: 0 0 auto;
}
.hero-label:hover {
  color: var(--fg);
  border-color: color-mix(in srgb, var(--accent-2) 55%, transparent);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
}
@keyframes heroFloat {
  0%, 100% { margin-top: 0; }
  50%      { margin-top: -6px; }
}

/* tiny contextual figures — the full stat grid lives below the hero */
/* On a card, like the labels around them. They used to be bare type with a
   text-shadow, which only reads over empty background — so they had to be
   pushed out to the far left of the figure to find some, and there they landed
   on top of the headline. A surface of their own lets them sit inside the
   figure where they belong, and over the suit if they have to. */
.hero-metric {
  position: absolute; transform: translate(-50%, -50%);
  font-family: "JetBrains Mono", monospace; text-align: center; line-height: 1.15;
  padding: 11px 16px; border-radius: 16px;
  border: 1px solid var(--brd); background: var(--panel);
  backdrop-filter: blur(14px);
  box-shadow: 0 12px 30px -18px rgba(0, 0, 0, .55);
  animation: heroFloat 16s ease-in-out infinite;
}
.hero-metric b {
  display: block; font-size: 19px; font-weight: 700; letter-spacing: -.02em;
  color: var(--fg);
}
.hero-metric span {
  display: block; margin-top: 3px; font-size: 9px; font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--fg-3);
}

@media (max-width: 1180px) {
  .hero-label { font-size: 11px; padding: 4px 9px 4px 7px; }
  .hero-metric b { font-size: 17px; }
}
@media (max-width: 960px) {
  .portrait-media { width: 100%; margin: 0 auto; aspect-ratio: 4 / 5; }
  .hero-net { inset: -4% -6%; width: 112%; height: 108%; }
}
@media (max-width: 640px) {
  .hero-metric { display: none; }
  .hero-label { font-size: 10.5px; }
  /* the network is allowed to bleed past the column on desktop; on a
     phone that bleed would give the page a horizontal scrollbar */
  .hero-net { inset: -2%; width: 104%; height: 104%; }
  .hero-labels { inset: -2%; }
  /* edge-anchored labels: no centring transform, so they cannot
     overhang the viewport on a narrow screen */
  .hero-label.at-left  { left: 2%;  transform: translateY(-50%); }
  .hero-label.at-right { right: 2%; left: auto; transform: translateY(-50%); }
  .hero-label { max-width: 62%; }
  .portrait-glow { inset: -8% -4% -6%; }
}
/* The hero's glow and network deliberately bleed past their column, so
   the clip has to happen at the viewport, not on .hero — clipping there
   cut the glow mid-gradient and showed as a hard vertical seam.
   `clip` (not `hidden`) does not create a scroll container, so the
   sticky nav keeps working. */
html, body { overflow-x: clip; }

/* reduced motion: keep the composition, drop the movement */
@media (prefers-reduced-motion: reduce) {
  .portrait-glow, .hero-label, .hero-metric { animation: none !important; }
  .portrait { --par-x: 0px !important; --par-y: 0px !important; }
}

/* ── panels & grids ───────────────────────────────────────── */
.panel { padding: clamp(24px, 3vw, 34px); border-radius: 24px; border: 1px solid var(--brd); background: var(--panel); backdrop-filter: blur(16px); }
.panel h2 { font-size: clamp(24px, 2.8vw, 34px); font-weight: 700; margin: 0; }
/* h2 and h3 both appear here: a panel that sits under the page h1 uses h2
   so the heading order does not skip a level, one nested inside a section
   with its own h2 uses h3. They are the same thing visually. */
.panel h3, .panel h2 { font-size: 20px; font-weight: 700; margin: 0 0 4px; }
.panel p { font-size: 15px; line-height: 1.75; color: var(--fg-2); }
.panel-accent { background: linear-gradient(140deg, color-mix(in srgb, var(--accent) 26%, transparent), color-mix(in srgb, var(--accent-2) 16%, transparent) 60%, transparent); padding: clamp(28px, 4vw, 52px); }
.panel-accent p { max-width: 58ch; }

.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); gap: 18px; }
.split-2 { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 18px; }
.split-teach { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); gap: 18px; align-items: start; }

.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(185px, 1fr)); gap: 16px; }
.stat { padding: 26px; border-radius: var(--radius); border: 1px solid var(--brd); background: var(--panel); backdrop-filter: blur(16px); }
.stat-value { font-size: 34px; font-weight: 800; letter-spacing: -.03em; margin: 0; font-variant-numeric: tabular-nums;
  background: linear-gradient(100deg, var(--accent), var(--accent-2)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.stat-label { font-size: 12.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; margin: 10px 0 0; color: var(--fg-2); }
.stat-source { font-size: 12px; margin: 3px 0 0; color: var(--fg-3); }

.project { display: block; padding: 28px; border-radius: var(--radius); border: 1px solid var(--brd);
  background: var(--panel); backdrop-filter: blur(16px); color: inherit; transition: transform .25s ease, border-color .25s ease; }
.project:hover { transform: translateY(-5px); border-color: var(--accent); color: inherit; }
.project h3, .project h2 { font-size: 20px; font-weight: 700; margin: 12px 0 0; }
.project p { font-size: 14px; line-height: 1.7; margin: 10px 0 0; color: var(--fg-2); }
.project .host { font-size: 13px; margin: 14px 0 0; color: var(--fg-3); font-family: "JetBrains Mono", monospace; }
.project .kicker { margin: 0; font-size: 11.5px; }

.book-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 18px; }
.book { padding: 16px; border-radius: var(--radius); border: 1px solid var(--brd); background: var(--panel); backdrop-filter: blur(16px); }
.book img { width: 100%; display: block; border-radius: 12px; }
.book h3 { font-size: 15px; font-weight: 600; line-height: 1.5; margin: 14px 0 0; }
.book p { font-size: 13.5px; margin: 8px 0 0; }

.link-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
.link-row { display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 17px 20px; border-radius: 14px; border: 1px solid var(--brd); background: var(--panel);
  backdrop-filter: blur(12px); color: var(--fg); font-size: 14.5px; }
.link-row:hover { border-color: var(--accent); color: var(--fg); }
.link-row .meta { font-size: 12px; color: var(--fg-3); font-family: "JetBrains Mono", monospace; }

.chips { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.chip { display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; padding: 9px 16px;
  border-radius: 999px; border: 1px solid var(--brd); background: color-mix(in srgb, var(--accent) 10%, transparent);
  font-size: 14px; color: var(--fg-2); }
.chip .n { color: var(--fg-3); font-family: "JetBrains Mono", monospace; font-size: 12.5px; }

/* ── publications ─────────────────────────────────────────── */
.toolbar { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.toolbar input[type="search"] { flex: 1; min-width: 240px; padding: 14px 18px; border-radius: 14px; border: 1px solid var(--brd);
  background: var(--panel); color: var(--fg); font-family: inherit; font-size: 15px; backdrop-filter: blur(12px); }
.filters { display: flex; gap: 10px; flex-wrap: wrap; }
/* the chips are a separate control from the search box, not a continuation of
   it, so they need room to read as one — and when they wrap to a second row the
   row gap has to be at least the chip gap or the rows fuse */
.toolbar + .filters { margin-top: 18px; }
.filter { padding: 12px 18px; border-radius: 12px; border: 1px solid var(--brd); background: var(--panel);
  color: var(--fg-2); font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer; }
a.filter { display: inline-flex; align-items: center; text-decoration: none; }
.filter:hover { border-color: var(--accent); color: var(--fg); }
.filter.is-active { border-color: var(--accent); color: var(--fg); background: color-mix(in srgb, var(--accent) 16%, transparent); }
.pub-list { display: flex; flex-direction: column; gap: 12px; margin-top: 18px; }
.pub { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px 24px; align-items: center;
  padding: 22px 26px; border-radius: 18px; border: 1px solid var(--brd); background: var(--panel);
  backdrop-filter: blur(14px); transition: border-color .2s ease; }
.pub:hover { border-color: var(--accent); }
.pub-tags { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin: 0 0 8px; }
.tag { font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; padding: 5px 10px;
  border-radius: 8px; background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.pub .meta { font-size: 12.5px; color: var(--fg-3); font-family: "JetBrains Mono", monospace; }
.pub h3, .pub h2 { font-size: 17px; font-weight: 600; line-height: 1.5; margin: 0; }
.pub .venue { font-size: 13.5px; margin: 6px 0 0; color: var(--fg-2); }
.pub-cites { text-align: right; }
.pub-cites strong { display: block; font-size: 26px; font-weight: 800; font-variant-numeric: tabular-nums; }
.pub-cites span { font-size: 11.5px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-3); }

/* ── teaching ─────────────────────────────────────────────── */
.quote { font-size: 19px; line-height: 1.6; margin: 18px 0 26px; color: var(--accent); }
#philosophy p { font-size: 15px; line-height: 1.8; margin: 0 0 18px; color: var(--fg-2); }
.course-list p { font-size: 14.5px; line-height: 1.9; margin: 0; color: var(--fg-2); }

/* ── workshops ────────────────────────────────────────────── */
.workshop { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 3fr); gap: 18px clamp(24px, 4vw, 56px);
  padding: clamp(26px, 3vw, 38px); margin: 0 0 16px; border-radius: 24px; border: 1px solid var(--brd);
  background: var(--panel); backdrop-filter: blur(16px); }
.workshop h3 { font-size: 22px; font-weight: 700; line-height: 1.35; margin: 0; max-width: 32ch; }
.workshop p { font-size: 14.5px; line-height: 1.8; margin: 14px 0 0; color: var(--fg-2); }
.workshop-meta { display: flex; flex-direction: column; gap: 10px; }
.workshop-meta p { font-size: 14px; margin: 0; color: var(--fg); }
.workshop-meta strong { display: block; font-size: 11px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--fg-3); }
.workshop-meta .btn { align-self: start; margin-top: 6px; padding: 12px 22px; font-size: 14px; }

/* ── charts ───────────────────────────────────────────────── */
.bars { display: flex; align-items: stretch; gap: 8px; margin-top: 18px; flex: 1; min-height: 190px; }
.bars .col { display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 8px; flex: 1; min-width: 0; }
/* the plot box the bar is a percentage of: it takes whatever height is left
   once the year label has its own, and carries the value label so that stays
   with its bar */
.bars .col-plot {
  flex: 1; width: 100%; min-height: 0;
  display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 6px;
}
.bars .col-plot .lbl { flex: 0 0 auto; }
.bars .bar { width: 100%; max-width: 34px; border-radius: 8px 8px 3px 3px;
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
  transform-origin: bottom; animation: grow .9s cubic-bezier(.22,1,.36,1) both; }
.bars .lbl { font-size: 11px; color: var(--fg-3); font-family: "JetBrains Mono", monospace; }
.hbars { display: flex; flex-direction: column; gap: 16px; margin-top: 18px; }
.hbar-head { display: flex; justify-content: space-between; font-size: 14px; color: var(--fg-2); margin-bottom: 8px; }
.hbar-head .n { font-family: "JetBrains Mono", monospace; color: var(--fg-3); }
.hbar-track { height: 10px; border-radius: 99px; background: var(--brd-2); overflow: hidden; }
.hbar-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--accent-2));
  transform-origin: left; animation: growx .8s cubic-bezier(.22,1,.36,1) both; }
/* the donut panel sits beside a much taller venue list, so centre its
   contents instead of letting them pool at the top of dead space */
.donut-wrap {
  display: flex; gap: 26px; align-items: center; justify-content: center;
  /* wrapping means align-content, not align-items, positions the line */
  align-content: center;
  flex-wrap: wrap; margin-top: 18px; min-height: 0; flex: 1;
}
.donut-wrap svg { flex: 0 0 auto; }
.donut-key { display: flex; flex-direction: column; gap: 9px; min-width: 150px; }
.donut-key div { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--fg-2); }
.donut-key .swatch { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.donut-key .n { font-family: "JetBrains Mono", monospace; color: var(--fg-3); }
svg.line-chart { width: 100%; height: auto; display: block; overflow: visible; margin-top: 18px; }

/* ── footer ───────────────────────────────────────────────── */
.footer { position: relative; z-index: 1; border-top: 1px solid var(--brd-2);
  background: color-mix(in srgb, var(--bg) 65%, transparent); backdrop-filter: blur(16px); }
.footer-inner { max-width: var(--wrap); margin: 0 auto; padding: 56px var(--pad) 36px;
  display: grid; gap: 36px;
  /* The first column has to hold six 42px social icons on one row — 6 icons +
     5 gaps is 302px — so it is given a floor rather than an equal share, which
     at four columns left it 269px and wrapped the last icon onto its own line. */
  grid-template-columns: minmax(302px, 1.4fr) repeat(3, minmax(170px, 1fr)); }
@media (max-width: 1080px) {
  .footer-inner { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px 36px; }
}
@media (max-width: 620px) {
  .footer-inner { grid-template-columns: minmax(0, 1fr); }
}
.footer h3 { font-size: 19px; font-weight: 700; margin: 0 0 14px; }
.footer p { font-size: 14px; line-height: 1.85; margin: 0; color: var(--fg-2); }
.stack p { margin: 0; }

/* the footer's workshop column: the date leads, because that is what decides
   whether the rest of the line is worth reading */
.upcoming a { display: block; }
.upcoming .when {
  display: block; font-family: "JetBrains Mono", monospace; font-size: 11px;
  color: var(--fg-3); letter-spacing: .02em; margin-bottom: 2px;
}
.upcoming .what { display: block; line-height: 1.45; }
.upcoming + .upcoming { margin-top: 12px; }
.upcoming-all { margin-top: 14px; font-weight: 600; }
.footer-legal { max-width: var(--wrap); margin: 0 auto; padding: 0 var(--pad) 44px; }
.footer-legal p { font-size: 12.5px; line-height: 1.9; margin: 0; color: var(--fg-3); }

/* ── footer social icons ──────────────────────────────────── */
.social-icons { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }
.social-icon {
  --brand: var(--accent);
  position: relative; isolation: isolate;
  display: grid; place-items: center; width: 42px; height: 42px;
  border-radius: 14px; color: var(--fg-2);
  background: var(--panel); border: 1px solid var(--brd);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07);
  transition: transform .3s cubic-bezier(.2, .8, .3, 1), color .3s ease,
    border-color .3s ease, box-shadow .3s ease;
}
/* brand-tinted bloom, painted behind the glyph but over the chip */
.social-icon::before {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
  background:
    radial-gradient(120% 100% at 50% 0%, rgba(255, 255, 255, .3), transparent 62%),
    linear-gradient(158deg, color-mix(in srgb, var(--brand) 82%, #fff), var(--brand) 52%,
      color-mix(in srgb, var(--brand) 86%, #000));
  opacity: 0; transition: opacity .3s ease;
}
.social-icon:hover, .social-icon:focus-visible {
  color: #fff; transform: translateY(-4px);
  border-color: color-mix(in srgb, var(--brand) 58%, transparent);
  box-shadow: 0 16px 30px -16px color-mix(in srgb, var(--brand) 90%, transparent),
    inset 0 1px 0 rgba(255, 255, 255, .18);
}
.social-icon:hover::before, .social-icon:focus-visible::before { opacity: 1; }
.social-icon svg { width: 18px; height: 18px; display: block; fill: currentColor; }
.social-icon .mono { font-size: 15px; font-weight: 700; line-height: 1; }
/* label rides above the chip on hover */
.social-icon::after {
  content: attr(data-label);
  position: absolute; top: calc(100% + 9px); left: 50%;
  transform: translate(-50%, -5px); padding: 5px 9px; border-radius: 9px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  white-space: nowrap; color: var(--fg); pointer-events: none; opacity: 0;
  background: color-mix(in srgb, var(--bg) 86%, var(--fg) 8%);
  border: 1px solid var(--brd);
  box-shadow: 0 12px 26px -16px rgba(0, 0, 0, .95);
  transition: opacity .24s ease, transform .24s ease;
}
.social-icon:hover::after, .social-icon:focus-visible::after { opacity: 1; transform: translate(-50%, 0); }
.social-icon[data-brand="facebook"]  { --brand: #1877f2; }
.social-icon[data-brand="x"]         { --brand: #5b6470; }
.social-icon[data-brand="linkedin"]  { --brand: #0a66c2; }
.social-icon[data-brand="instagram"] { --brand: #e1306c; }
.social-icon[data-brand="youtube"]   { --brand: #ff0033; }
.social-icon[data-brand="tiktok"]    { --brand: #00c8c0; }
.social-icon[data-brand="telegram"]  { --brand: #229ed9; }
.social-icon[data-brand="whatsapp"]  { --brand: #1fb355; }
.social-icon[data-brand="github"]    { --brand: #6e7681; }
.social-icon[data-brand="threads"]   { --brand: #6b7280; }
@media (prefers-reduced-motion: reduce) {
  .social-icon, .social-icon::before, .social-icon::after { transition: none; }
  .social-icon:hover, .social-icon:focus-visible { transform: none; }
}

/* ── animation ────────────────────────────────────────────── */
@keyframes rise { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes growx { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes drift { 0%, 100% { transform: translate3d(0,0,0) scale(1); } 50% { transform: translate3d(2%,-3%,0) scale(1.06); } }
@keyframes pulse { 0%, 100% { opacity: .45; } 50% { opacity: .9; } }
@keyframes sweep { from { stroke-dashoffset: 1400; } to { stroke-dashoffset: 0; } }

.reveal { opacity: 0; }
.reveal.is-in { animation: rise .8s cubic-bezier(.22,1,.36,1) both; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; }
  .reveal { opacity: 1; }
}

/* ── responsive ───────────────────────────────────────────── */
@media (max-width: 960px) {
  .hero, .split-2, .split-teach, .workshop { grid-template-columns: 1fr; }
  .portrait { max-width: 460px; margin: 0 auto; }
}
@media (max-width: 720px) {
  .nav-links a[data-route] { padding: 6px 10px; font-size: 13.5px; }
  .pub { grid-template-columns: 1fr; }
  .pub-cites { text-align: left; }
}

/* ============================================================
   Pages ported from the previous site: book detail, legal,
   PhD resources, and the journal-list tables.
   ============================================================ */

/* ── shared document layout ───────────────────────────────── */
.doc .prose { max-width: 74ch; }
.prose p { font-size: 15.5px; line-height: 1.8; color: var(--fg-2); }
.prose h2 { font-size: clamp(21px, 2.2vw, 27px); font-weight: 700; margin: 38px 0 10px; color: var(--fg); }
.prose strong { color: var(--fg); font-weight: 600; }
.prose ul { padding-left: 20px; }
.prose li { font-size: 15.5px; line-height: 1.8; color: var(--fg-2); margin-bottom: 10px; }
.back a { font-size: 14px; color: var(--fg-3); }
.back a:hover { color: var(--accent); }
.byline { font-size: 14px; color: var(--fg-3); margin: 0 0 22px; }

.shot { margin: 22px 0 0; }
.shot img {
  width: 100%; display: block; border-radius: 14px;
  border: 1px solid var(--brd); background: var(--panel);
}
/* `"bg": "light"` on an image block. A QR code is black modules on nothing:
   on the panel background it is a black square on a dark square, and no
   scanner will read it. It needs a white plate and a quiet zone in both
   themes, not the theme's own surface. */
.shot.is-light img {
  background: #fff; padding: 14px; box-sizing: border-box; border-color: transparent;
}

/* ── book detail ──────────────────────────────────────────── */
.book-hero { display: grid; grid-template-columns: minmax(0, 300px) minmax(0, 1fr); gap: clamp(26px, 4vw, 56px); align-items: start; }
.book-cover img {
  width: 100%; display: block; border-radius: 16px;
  border: 1px solid var(--brd); box-shadow: var(--glow);
}
.book-hero .page-title { margin-top: 6px; }
.book-hero .row { margin-top: 22px; flex-wrap: wrap; }

/* the overview grid tiles are now links */
a.book { display: block; color: inherit; transition: transform .25s ease, border-color .25s ease; }
a.book:hover { transform: translateY(-5px); border-color: var(--accent); color: inherit; }
a.book p { font-size: 13px; color: var(--accent); margin: 8px 0 0; }

.specs { display: grid; grid-template-columns: auto 1fr; gap: 10px 22px; margin: 14px 0 0; }
.specs dt { font-size: 12.5px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3); }
.specs dd { margin: 0; font-size: 15px; color: var(--fg-2); }

.faq { display: grid; gap: 10px; margin-top: 18px; }
.faq details {
  border: 1px solid var(--brd); border-radius: 14px;
  background: var(--panel); backdrop-filter: blur(16px); overflow: hidden;
}
.faq summary {
  cursor: pointer; padding: 16px 20px; font-weight: 600; font-size: 15px;
  list-style: none; display: flex; justify-content: space-between; gap: 16px;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; color: var(--accent); font-weight: 400; font-size: 20px; line-height: 1; }
.faq details[open] summary::after { content: "−"; }
.faq details > div { padding: 0 20px 18px; font-size: 14.5px; line-height: 1.75; color: var(--fg-2); }

/* ── legal ────────────────────────────────────────────────── */
.policy { margin: 18px 0 0; }
.policy dt { font-weight: 700; font-size: 15.5px; margin-top: 22px; color: var(--fg); }
.policy dd { margin: 6px 0 0; font-size: 15.5px; line-height: 1.8; color: var(--fg-2); }

/* ── resource steps ───────────────────────────────────────── */
.steps { counter-reset: step; list-style: none; padding: 0; margin: 22px 0 0; }
.steps li { counter-increment: step; position: relative; padding-left: 46px; margin-bottom: 30px; }
.steps li::before {
  content: counter(step); position: absolute; left: 0; top: 0;
  width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center; font-size: 14px; font-weight: 700;
  color: var(--bg); background: linear-gradient(140deg, var(--accent), var(--accent-2));
}
.steps li p { margin: 4px 0 0; font-size: 15.5px; line-height: 1.8; color: var(--fg-2); }

/* ── journal tables ───────────────────────────────────────── */
.table-wrap { overflow-x: auto; border: 1px solid var(--brd); border-radius: var(--radius); background: var(--panel); backdrop-filter: blur(16px); margin-top: 6px; }
.jtable { width: 100%; border-collapse: collapse; font-size: 14px; min-width: 780px; }
.jtable th {
  text-align: left; padding: 14px 16px; font-size: 11.5px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--fg-3);
  border-bottom: 1px solid var(--brd); white-space: nowrap; position: sticky; top: 0;
  background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(16px);
}
.jtable th.sortable { cursor: pointer; user-select: none; }
.jtable th.sortable:hover, .jtable th.is-sorted { color: var(--accent); }
.jtable th .arrow { margin-left: 6px; font-size: 11px; }
.jtable td { padding: 13px 16px; border-bottom: 1px solid var(--brd-2); color: var(--fg-2); vertical-align: top; }
.jtable td.num { font-family: "JetBrains Mono", monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
.jtable tbody tr[data-idx] { cursor: pointer; transition: background .18s ease; }
.jtable tbody tr[data-idx]:hover { background: color-mix(in srgb, var(--accent) 9%, transparent); }
.jtable tbody tr.is-open { background: color-mix(in srgb, var(--accent) 13%, transparent); }
.jtable tbody tr.is-open td { color: var(--fg); font-weight: 600; border-bottom-color: transparent; }
.jtable tr.detail-row td { background: color-mix(in srgb, var(--accent) 5%, transparent); padding: 20px 16px 24px; }

.q { display: inline-block; padding: 3px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 700; letter-spacing: .04em; }
.q.q1 { background: color-mix(in srgb, #22c55e 26%, transparent); color: #4ade80; }
.q.q2 { background: color-mix(in srgb, var(--accent-3) 26%, transparent); color: var(--accent-3); }
.q.q3 { background: color-mix(in srgb, #f59e0b 26%, transparent); color: #fbbf24; }
.q.q4 { background: color-mix(in srgb, var(--accent-2) 24%, transparent); color: var(--accent-2); }
html[data-theme="light"] .q.q1 { color: #15803d; }
html[data-theme="light"] .q.q3 { color: #a16207; }

.detail { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; }
.dpanel { border: 1px solid var(--brd); border-radius: 14px; padding: 16px 18px; background: var(--panel); }
.dpanel h2, .dpanel h4 { margin: 0 0 10px; font-size: 11.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); }
.dpanel dl { display: grid; grid-template-columns: auto 1fr; gap: 7px 16px; margin: 0; }
.dpanel dt { font-size: 12.5px; color: var(--fg-3); }
.dpanel dd { margin: 0; font-size: 13.5px; color: var(--fg); text-align: right; font-variant-numeric: tabular-nums; }
.dpanel.dbrief { grid-column: 1 / -1; }
.dpanel.dbrief p { margin: 0; font-size: 14px; line-height: 1.7; color: var(--fg-2); }
.detail-links { margin-top: 16px; flex-wrap: wrap; }

.pager { display: flex; align-items: center; justify-content: center; gap: 18px; margin-top: 20px; }
.pager .btn[disabled] { opacity: .38; pointer-events: none; }
.pager .note { margin: 0; font-variant-numeric: tabular-nums; }

/* ── QR payment overlay ───────────────────────────────────── */
.qr-overlay {
  position: fixed; inset: 0; z-index: 90; display: grid; place-items: center;
  background: rgba(3, 1, 10, .72); backdrop-filter: blur(6px); padding: 24px;
}
.qr-overlay[hidden] { display: none; }
.qr-box {
  background: var(--bg); border: 1px solid var(--brd); border-radius: var(--radius);
  padding: 28px; text-align: center; max-width: 400px; box-shadow: var(--glow);
}
.qr-box h3 { margin: 0 0 18px; font-size: 18px; }
.qr-box img { width: 100%; max-width: 320px; border-radius: 12px; display: block; margin: 0 auto 20px; }

@media (max-width: 720px) {
  .book-hero { grid-template-columns: 1fr; }
  .book-cover { max-width: 220px; }
  .dpanel dd { text-align: left; }
}


/* ============================================================
   Publication library: rich records driven by
   assets/data/publications.json
   ============================================================ */

.toolbar .sort-wrap { display: flex; align-items: center; gap: 10px; }
.toolbar .sort-wrap span { font-size: 12.5px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--fg-3); }
.toolbar select {
  padding: 13px 16px; border-radius: 14px; border: 1px solid var(--brd);
  background: var(--panel); color: var(--fg); font-family: inherit; font-size: 14.5px;
  font-weight: 600; cursor: pointer; backdrop-filter: blur(12px);
}
.load-more { display: block; margin: 22px auto 0; }
.load-more[hidden] { display: none; }

.pub { transition: border-color .22s ease, background .22s ease; }
.pub h3, .pub h2 { font-size: 17.5px; line-height: 1.45; margin: 10px 0 0; }
.pub h3 a, .pub h2 a { color: inherit; }
.pub h3 a:hover, .pub h2 a:hover { color: var(--accent); }
.pub .authors { font-size: 13.5px; line-height: 1.7; color: var(--fg-2); margin: 8px 0 0; }
.pub .authors .me { color: var(--fg); font-weight: 700; }
.pub .venue { font-size: 13px; color: var(--fg-3); margin: 6px 0 0; }
.pub-tags { flex-wrap: wrap; gap: 8px; align-items: center; }

.badge {
  display: inline-block; padding: 3px 10px; border-radius: 999px;
  font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
}
.badge.oa    { background: color-mix(in srgb, #22c55e 24%, transparent); color: #4ade80; }
.badge.first { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent); }
.badge.q.q1  { background: color-mix(in srgb, #22c55e 24%, transparent); color: #4ade80; }
.badge.q.q2  { background: color-mix(in srgb, var(--accent-3) 24%, transparent); color: var(--accent-3); }
.badge.q.q3  { background: color-mix(in srgb, #f59e0b 26%, transparent); color: #fbbf24; }
.badge.q.q4  { background: color-mix(in srgb, var(--accent-2) 22%, transparent); color: var(--accent-2); }
html[data-theme="light"] .badge.oa,
html[data-theme="light"] .badge.q.q1 { color: #15803d; }
html[data-theme="light"] .badge.q.q3 { color: #a16207; }
/* the venue's ABDC / AJG rating on a publication card borrows the rating ramp
   the journal lists use, in the pill shape the other badges have */
.badge.rate { border-radius: 999px; padding: 3px 10px; font-family: inherit; }

/* the whole card opens the record now, so it reads as a control */
.pub { cursor: pointer; }
.pub:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.pub h3 a, .pub h2 a { cursor: pointer; }

.pub-links { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; margin: 14px 0 0; }
.pub-links a, .pub-links .more {
  font-size: 13px; font-weight: 600; color: var(--accent);
  background: none; border: 0; padding: 0; cursor: pointer; font-family: inherit;
}
.pub-links a:hover, .pub-links .more:hover { color: var(--accent-2); }
.pub-links a.dl {
  padding: 6px 14px; border-radius: 999px;
  background: color-mix(in srgb, #22c55e 18%, transparent); color: #4ade80;
}
.pub-links a.dl:hover { background: color-mix(in srgb, #22c55e 30%, transparent); color: #86efac; }
html[data-theme="light"] .pub-links a.dl { color: #15803d; }

.pub-cites .fwci {
  display: block; margin-top: 8px; font-size: 11px; font-weight: 700;
  letter-spacing: .04em; color: var(--accent); white-space: nowrap;
}

.pub-detail { margin: 18px 0 0; padding-top: 18px; border-top: 1px solid var(--brd-2); }
.pub-detail p { font-size: 14px; line-height: 1.75; color: var(--fg-2); margin: 0 0 16px; }
.pub-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.pub-facts div {
  padding: 12px 14px; border-radius: 12px;
  border: 1px solid var(--brd-2); background: color-mix(in srgb, var(--accent) 5%, transparent);
}
.pub-facts span { display: block; font-size: 11px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--fg-3); }
.pub-facts b { font-size: 17px; font-weight: 800; font-variant-numeric: tabular-nums; }

.pub-cites-split { margin-top: 16px; }
.pub-cites-split > div { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 8px; }
.pub-cites-split span { font-size: 12.5px; color: var(--fg-3); font-variant-numeric: tabular-nums; }
.pub-cites-split b { color: var(--fg); font-size: 14px; }

.chips.sm { margin-top: 16px; gap: 6px; }
.chips.sm .chip { font-size: 11.5px; padding: 5px 11px; }
.note .ok { color: #4ade80; font-weight: 700; }
html[data-theme="light"] .note .ok { color: #15803d; }
.note strong { color: var(--fg); }

@media (max-width: 720px) {
  .toolbar .sort-wrap { width: 100%; justify-content: space-between; }
  .pub-facts { grid-template-columns: 1fr 1fr; }
}

/* long year spans: tighter columns, smaller labels */
.bars.dense { gap: 4px; }
.bars.dense .col .lbl { font-size: 9.5px; }
.bars.dense .bar { min-width: 6px; }


/* ============================================================
   Products, about, grants, supervision, service
   ============================================================ */

/* ── product showcase ─────────────────────────────────────── */
.product-grid {
  display: grid; gap: 20px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.product-grid > :first-child { grid-column: span 2; }
@media (max-width: 900px) { .product-grid > :first-child { grid-column: span 1; } }

.product {
  --brand: var(--accent);
  position: relative; display: flex; flex-direction: column;
  padding: 30px; border-radius: 26px; overflow: hidden;
  border: 1px solid var(--brd); background: var(--panel);
  backdrop-filter: blur(16px); color: inherit;
  transition: transform .3s cubic-bezier(.22,1,.36,1), border-color .3s ease;
}
.product::before {
  content: ""; position: absolute; inset: -40% 40% 55% -20%;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--brand) 40%, transparent), transparent 75%);
  opacity: .5; pointer-events: none; transition: opacity .35s ease;
}
.product::after {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--brand), transparent);
  opacity: .55;
}
.product:hover { transform: translateY(-7px); border-color: var(--brand); color: inherit; }
.product:hover::before { opacity: .85; }
.product > * { position: relative; }

.product-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 22px; }
.product-mark {
  display: grid; place-items: center; width: 48px; height: 48px; border-radius: 15px;
  font-family: "JetBrains Mono", monospace; font-size: 15px; font-weight: 700; letter-spacing: -.02em;
  color: var(--brand); border: 1px solid color-mix(in srgb, var(--brand) 45%, transparent);
  background: color-mix(in srgb, var(--brand) 15%, transparent);
}
.product-since {
  font-family: "JetBrains Mono", monospace; font-size: 11.5px; color: var(--fg-3);
}
.product h3 { font-size: 24px; font-weight: 800; letter-spacing: -.02em; margin: 0; }
.product-tag {
  margin: 6px 0 14px; font-size: 13px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--brand);
}
.product > p { font-size: 14.5px; line-height: 1.75; color: var(--fg-2); margin: 0; }
.product-points { list-style: none; padding: 0; margin: 18px 0 0; display: grid; gap: 8px; }
.product-points li {
  position: relative; padding-left: 20px; font-size: 13.5px; color: var(--fg-2);
}
.product-points li::before {
  content: ""; position: absolute; left: 0; top: .55em; width: 7px; height: 7px;
  border-radius: 50%; background: var(--brand);
}
.product-host {
  margin-top: auto; padding-top: 22px;
  font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: var(--fg-3);
}
.product-host em { font-style: normal; color: var(--brand); }
.product:hover .product-host { color: var(--fg-2); }

/* ── about ────────────────────────────────────────────────── */
.about-hero {
  display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  gap: clamp(28px, 4vw, 60px); align-items: start;
}
.about-hero .row { margin-top: 26px; }
.about-hero .chips { margin-top: 24px; }
.about-card {
  padding: 22px; border-radius: 24px; border: 1px solid var(--brd);
  background: var(--panel); backdrop-filter: blur(16px); box-shadow: var(--glow);
}
/* the portrait is a transparent cut-out, so give it a soft ground
   rather than a hard photo frame */
.about-photo {
  position: relative; margin-bottom: 18px; border-radius: 18px; overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 12%,
      color-mix(in srgb, var(--accent) 22%, transparent),
      color-mix(in srgb, var(--accent-2) 10%, transparent) 55%,
      transparent 78%);
}
.about-photo img {
  width: 100%; display: block;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 76%, rgba(0,0,0,.5) 90%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 76%, rgba(0,0,0,.5) 90%, transparent 100%);
}
/* fallback path: a flattened JPEG needs the frame back */
.about-photo.is-flat { background: none; }
.about-photo.is-flat img {
  border-radius: 16px; -webkit-mask-image: none; mask-image: none;
}
.about-card dl { display: grid; gap: 14px; margin: 0; }
.about-card dt {
  font-size: 11px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--fg-3);
}
.about-card dd { margin: 3px 0 0; font-size: 13.5px; line-height: 1.6; color: var(--fg-2); }

.timeline { position: relative; margin-top: 22px; padding-left: 26px; }
.timeline::before {
  content: ""; position: absolute; left: 5px; top: 6px; bottom: 6px; width: 1px;
  background: linear-gradient(180deg, var(--accent), var(--accent-2), transparent);
  opacity: .45;
}
.tl { position: relative; padding-bottom: 26px; }
.tl::before {
  content: ""; position: absolute; left: -25px; top: 5px; width: 11px; height: 11px;
  border-radius: 50%; background: var(--bg);
  border: 2px solid color-mix(in srgb, var(--accent) 65%, transparent);
}
.tl.is-now::before { background: var(--accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 22%, transparent); }
.tl-when {
  font-family: "JetBrains Mono", monospace; font-size: 11.5px; color: var(--fg-3);
  letter-spacing: .02em;
}
.tl h3, .tl h4 { font-size: 16px; font-weight: 700; margin: 5px 0 3px; }
.tl p { font-size: 13.5px; line-height: 1.6; color: var(--fg-2); margin: 0; }

.link-row.static { cursor: default; }
.link-row.static:hover { border-color: var(--brd); }

.award-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
.award {
  padding: 22px; border-radius: var(--radius); border: 1px solid var(--brd);
  background: var(--panel); backdrop-filter: blur(16px);
  transition: border-color .25s ease, transform .25s ease;
}
.award:hover { border-color: var(--accent); transform: translateY(-3px); }
.award-year {
  font-family: "JetBrains Mono", monospace; font-size: 12px; font-weight: 600; color: var(--accent);
}
.award h3, .award h4 { font-size: 15px; font-weight: 700; line-height: 1.45; margin: 8px 0 0; }
.award-detail { font-size: 13px; font-style: italic; color: var(--fg-2); margin: 6px 0 0; }
.award-org { font-size: 12.5px; color: var(--fg-2); margin: 8px 0 0; }

/* ── grants ───────────────────────────────────────────────── */
.linkish {
  background: none; border: 0; padding: 0; cursor: pointer; font-family: inherit;
  font-size: 14px; font-weight: 600; color: var(--accent);
}
.linkish:hover { color: var(--accent-2); }

.grant-list { display: flex; flex-direction: column; gap: 12px; margin-top: 6px; }
.grant {
  display: grid; grid-template-columns: 62px 1fr auto; gap: 22px; align-items: center;
  padding: 20px 24px; border-radius: 18px; border: 1px solid var(--brd);
  background: var(--panel); backdrop-filter: blur(16px);
  transition: border-color .22s ease, background .22s ease;
}
.grant:hover { border-color: var(--accent); }
.grant-year {
  font-family: "JetBrains Mono", monospace; font-size: 14px; font-weight: 700; color: var(--accent);
}
.grant h3, .grant h4 { font-size: 15px; font-weight: 600; line-height: 1.5; margin: 0; }
.grant .meta { display: block; margin-top: 5px; font-size: 12.5px; }
.grant-amount {
  font-size: 17px; font-weight: 800; white-space: nowrap;
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}

/* ── supervision and service ──────────────────────────────── */
.sup-list { display: flex; flex-direction: column; gap: 12px; margin-top: 20px; }
.sup {
  padding: 18px 22px; border-radius: 16px; border: 1px solid var(--brd);
  background: var(--panel); backdrop-filter: blur(16px);
}
.sup-status {
  display: inline-block; padding: 3px 10px; border-radius: 999px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  background: color-mix(in srgb, var(--fg-3) 20%, transparent); color: var(--fg-2);
}
.sup-status.active { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent); }
.sup-status.graduated, .sup-status.completed {
  background: color-mix(in srgb, #22c55e 22%, transparent); color: #4ade80;
}
html[data-theme="light"] .sup-status.graduated,
html[data-theme="light"] .sup-status.completed { color: #15803d; }
.sup h3, .sup h4 { font-size: 15.5px; font-weight: 700; margin: 10px 0 4px; }
.sup p { font-size: 13px; line-height: 1.65; color: var(--fg-2); margin: 0; }

.service-list { margin-top: 20px; }
.service-head {
  font-size: 11.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--fg-3); margin: 0 0 12px;
}
.service {
  display: grid; grid-template-columns: auto 1fr auto; gap: 14px; align-items: baseline;
  padding: 12px 0; border-bottom: 1px solid var(--brd-2);
}
.service:last-child { border-bottom: 0; }
.service-role {
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--accent); white-space: nowrap;
}
.service strong { font-size: 13.5px; font-weight: 600; line-height: 1.5; }
.service-when {
  font-family: "JetBrains Mono", monospace; font-size: 11.5px; color: var(--fg-3); white-space: nowrap;
}

/* ── training ─────────────────────────────────────────────── */
/* the cards used to lead with a count; there is no defensible number to put
   there, so they lead with the subject instead */
.train { position: relative; }

@media (max-width: 900px) {
  .about-hero { grid-template-columns: 1fr; }
  .about-card { max-width: 380px; }
}
@media (max-width: 720px) {
  /* long topic labels must wrap rather than force a horizontal scroll */
  .chip { white-space: normal; max-width: 100%; }
  /* 23 year labels do not fit a phone: drop every other one so the columns
     can shrink (display:none, not visibility, or they still reserve width) */
  .bars.dense { gap: 3px; }
  .bars.dense .col:nth-child(even) .lbl { display: none; }
  .grant { grid-template-columns: 1fr auto; gap: 12px; }
  .grant-year { grid-column: 1 / -1; }
  .service { grid-template-columns: 1fr; gap: 4px; }
}

/* featured card: split the body into two columns so the wide card fills */
.product.is-feature { padding: 36px; }
.product.is-feature h3 { font-size: 30px; }
.product.is-feature .product-mark { width: 56px; height: 56px; border-radius: 17px; font-size: 17px; }
.product.is-feature .product-body {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr);
  gap: 34px; align-items: start;
}
.product.is-feature .product-body > p { font-size: 15.5px; }
.product.is-feature .product-points { margin-top: 0; gap: 11px; }
.product.is-feature .product-points li { font-size: 14px; }
@media (max-width: 900px) {
  .product.is-feature .product-body { grid-template-columns: 1fr; gap: 18px; }
  .product.is-feature { padding: 30px; }
  .product.is-feature h3 { font-size: 24px; }
}

/* ── product thumbnails ───────────────────────────────────── */
.product { padding: 0; }
.product-text { display: flex; flex-direction: column; flex: 1; padding: 30px; }
.product.is-feature .product-text { padding: 36px; }

/* the shot is a browser-chrome framed crop of the live homepage */
.product-shot {
  position: relative; margin: 14px 14px 0; border-radius: 16px; overflow: hidden;
  border: 1px solid var(--brd-2);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  aspect-ratio: 16 / 9;
}
.product-shot::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 55%, color-mix(in srgb, var(--panel) 92%, transparent));
  pointer-events: none;
}
.product-shot img {
  width: 100%; height: 100%; object-fit: cover; object-position: 50% 0;
  display: block; transition: transform .55s cubic-bezier(.22,1,.36,1);
}
.product:hover .product-shot img { transform: scale(1.035); }

/* the featured card keeps the shot on top, full width, so the homepage is
   shown whole rather than cropped into a narrow column */
.product.is-feature .product-shot { margin: 16px 16px 0; border-radius: 18px; }
@media (max-width: 900px) {
  .product-text, .product.is-feature .product-text { padding: 26px; }
}

/* brand panel stands in for a card with no live thumbnail */
.product-shot.is-blank {
  display: grid; place-items: center;
  background:
    radial-gradient(120% 120% at 50% 0%, color-mix(in srgb, var(--brand) 26%, transparent), transparent 70%),
    var(--panel);
}
.product-shot.is-blank::after { display: none; }
.product-shot.is-blank span {
  font-family: "JetBrains Mono", monospace; font-size: 46px; font-weight: 700;
  letter-spacing: -.03em; color: color-mix(in srgb, var(--brand) 70%, transparent);
}


/* ============================================================
   Notes
   ============================================================ */
.note-count {
  font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: var(--fg-3);
  white-space: nowrap;
}

/* Posts left, resource rail right. The rail is content, not decoration, so it
   sits above the fold on a wide screen and below the posts on a narrow one
   rather than being hidden. */
.blog-layout {
  display: grid; gap: 28px; align-items: start;
  grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
}
.blog-rail {
  position: sticky; top: 92px; padding: 22px; border-radius: 20px;
  border: 1px solid var(--brd); background: var(--panel); backdrop-filter: blur(16px);
}
.rail-head { margin-bottom: 18px; }
.rail-head .note { margin: 6px 0 0; font-size: 12.5px; line-height: 1.6; }
.rail-group + .rail-group { margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--brd-2); }
.rail-group h3, .rail-group h4 {
  margin: 0 0 10px; font-size: 10.5px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--fg-3);
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
}
.rail-group h3 span, .rail-group h4 span {
  font-family: "JetBrains Mono", monospace; font-size: 10.5px; letter-spacing: 0;
}
.rail-group ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.rail-group a {
  display: block; font-size: 13px; line-height: 1.5; color: var(--fg-2);
  padding-left: 12px; position: relative;
}
.rail-group a::before {
  content: ""; position: absolute; left: 0; top: .62em; width: 4px; height: 4px;
  border-radius: 50%; background: color-mix(in srgb, var(--accent) 55%, transparent);
}
.rail-group a:hover { color: var(--fg); }
.rail-group a:hover::before { background: var(--accent); }
.rail-src {
  display: block; font-family: "JetBrains Mono", monospace; font-size: 10px;
  color: var(--fg-3); margin-top: 2px;
}

.note-grid {
  display: grid; gap: 16px; margin-top: 6px;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.note-card {
  display: flex; flex-direction: column; padding: 0;
  border-radius: 20px; border: 1px solid var(--brd); background: var(--panel);
  backdrop-filter: blur(16px); color: inherit; position: relative; overflow: hidden;
  transition: transform .28s cubic-bezier(.22,1,.36,1), border-color .28s ease;
}
/* the card art doubles as the og:image; without one the slot is a tinted panel
   carrying the post's first tag, which reads as designed rather than as missing */
.note-art { display: block; aspect-ratio: 16 / 9; overflow: hidden; position: relative; }
.note-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.note-art.is-blank {
  display: grid; place-items: center;
  background:
    radial-gradient(120% 140% at 20% 0%, color-mix(in srgb, var(--accent) 26%, transparent), transparent 62%),
    radial-gradient(120% 140% at 100% 100%, color-mix(in srgb, var(--accent-2) 22%, transparent), transparent 60%),
    color-mix(in srgb, var(--fg) 4%, transparent);
}
.note-art.is-blank em {
  font-style: normal; font-size: 11px; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: color-mix(in srgb, var(--fg) 62%, transparent);
}
.note-body { display: flex; flex-direction: column; gap: 10px; padding: 20px 22px 22px; flex: 1; }
.note-card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  opacity: 0; transition: opacity .28s ease;
}
.note-card:hover { transform: translateY(-4px); border-color: var(--accent); color: inherit; }
.note-card:hover::before { opacity: .8; }
.note-card h3, .note-card h2 { font-size: 17px; font-weight: 700; line-height: 1.4; margin: 0; }
.note-card p {
  font-size: 13.5px; line-height: 1.65; color: var(--fg-2); margin: 0;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.note-card-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.note-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.note-chip {
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--accent); padding: 4px 9px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 13%, transparent);
}
.note-badge {
  font-family: "JetBrains Mono", monospace; font-size: 10.5px; color: var(--fg-3);
  padding: 3px 8px; border-radius: 999px; border: 1px solid var(--brd-2); white-space: nowrap;
}
.note-badge.is-locked {
  color: #fbbf24; border-color: color-mix(in srgb, #fbbf24 40%, transparent);
  background: color-mix(in srgb, #fbbf24 12%, transparent);
}
html[data-theme="light"] .note-badge.is-locked { color: #b45309; }
.note-foot {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin-top: auto; padding-top: 8px;
}
.note-foot .meta { font-size: 12px; }
.note-foot em {
  font-style: normal; font-size: 13px; font-weight: 600; color: var(--accent); white-space: nowrap;
}
.note-card.is-external { border-style: dashed; }

.note-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; }
.note-meta .chips.sm { margin-top: 0; }

.empty { color: var(--fg-2); font-size: 15px; }

/* callouts inside a note */
.callout {
  margin: 22px 0; padding: 18px 22px; border-radius: 14px;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  font-size: 14.5px; line-height: 1.7; color: var(--fg-2);
}
.callout strong { color: var(--fg); }
/* The workshop pages open with a red requirement banner. Reusing the accent
   callout for it lost the one thing it was doing — reading as a warning before
   the words are read. */
.callout.is-warn {
  border-color: color-mix(in srgb, #ef4444 40%, transparent);
  background: color-mix(in srgb, #ef4444 10%, transparent);
}
.callout.is-warn strong { color: #fca5a5; }
html[data-theme="light"] .callout.is-warn strong { color: #b91c1c; }
.prose figure.shot figcaption {
  margin-top: 8px; font-size: 12.5px; color: var(--fg-3); text-align: center;
}
.prose code {
  font-family: "JetBrains Mono", monospace; font-size: .88em;
  padding: 2px 6px; border-radius: 6px;
  background: color-mix(in srgb, var(--fg) 8%, transparent);
}

/* passcode gate */
/* Full width, like the panels above and below it. At 460px in a 1,184px column
   it read as a widget dropped into the page rather than a part of it. */
.gate {
  margin: 10px 0; padding: clamp(34px, 5vw, 46px) 32px; text-align: center;
  border-radius: 24px; border: 1px solid var(--brd); background: var(--panel);
  backdrop-filter: blur(16px); box-shadow: var(--glow);
}
.gate-icon { font-size: 13px; letter-spacing: .35em; color: var(--accent); margin-bottom: 18px; }
.gate h2 { font-size: 21px; font-weight: 700; margin: 0 0 8px; }
.gate > p { font-size: 14px; line-height: 1.7; color: var(--fg-2); margin: 0 0 22px; }
/* the form itself stays a comfortable reading width inside the wide panel */
.gate-form { display: flex; gap: 10px; max-width: 440px; margin: 0 auto; }
.gate-form input {
  flex: 1; min-width: 0; padding: 13px 16px; border-radius: 13px;
  border: 1px solid var(--brd); background: var(--bg); color: var(--fg);
  font-family: inherit; font-size: 15px;
}
.gate-form input:focus { outline: none; border-color: var(--accent); }
/* `.gate > p` is more specific than `.gate-cta`, so without this the link
   inherits that rule's `0 0 22px`: no room above it, and 22px of dead space
   below the last line of the panel. */
.gate > .gate-cta { margin: 18px 0 0; }
.gate-err { margin: 14px 0 0; font-size: 13.5px; color: #f87171; }
html[data-theme="light"] .gate-err { color: #b91c1c; }

@media (max-width: 720px) {
  .gate { padding: 32px 22px; }
  .gate-form { flex-direction: column; }
}


/* shown when the publication database cannot be fetched — most often
   because index.html was opened from disk instead of over http */
.loaderr {
  grid-column: 1 / -1;
  padding: 28px 30px; border-radius: 20px; text-align: left;
  border: 1px solid color-mix(in srgb, #fbbf24 45%, transparent);
  background: color-mix(in srgb, #fbbf24 9%, transparent);
}
.loaderr h3 { font-size: 18px; font-weight: 700; margin: 0 0 10px; color: var(--fg); }
.loaderr p { font-size: 14px; line-height: 1.7; color: var(--fg-2); margin: 0 0 12px; }
.loaderr p:last-child { margin-bottom: 0; }
.loaderr code {
  font-family: "JetBrains Mono", monospace; font-size: .88em; padding: 2px 6px;
  border-radius: 6px; background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.loaderr pre {
  font-family: "JetBrains Mono", monospace; font-size: 13px; margin: 0 0 12px;
  padding: 14px 16px; border-radius: 12px; overflow-x: auto;
  background: color-mix(in srgb, var(--fg) 10%, transparent); color: var(--fg);
}
.loaderr a { color: var(--accent); font-weight: 600; }


/* supervision runs full width now that editorial service is gone —
   two columns keeps 14 cards from becoming one long ribbon */
.sup-list.is-cols {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 12px;
}


/* ============================================================
   Interactive charts
   ============================================================ */
.chart-read {
  font-size: 13.5px; color: var(--fg-2); margin-bottom: 12px; min-height: 20px;
  font-variant-numeric: tabular-nums;
}
.chart-read strong { color: var(--fg); font-weight: 700; }

/* a year column is a control: hover or focus to read it, click to
   filter the publication library to that year */
.bars .col.is-live { cursor: pointer; border-radius: 8px; }
.bars .col.is-live:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.bars.is-hover .col .bar { opacity: .38; }
.bars .col.is-on .bar,
.bars .col.is-live:hover .bar {
  opacity: 1;
  filter: brightness(1.18) saturate(1.15);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent),
              0 6px 20px color-mix(in srgb, var(--accent) 35%, transparent);
}
.bars .col .bar { transition: opacity .18s ease, filter .18s ease, box-shadow .18s ease; }
.bars .col.is-on .lbl,
.bars .col.is-live:hover .lbl { color: var(--fg); }

/* tighter panels so the analytics page stops feeling airy */
.panel { padding: clamp(20px, 2.2vw, 26px); }
.split-2 { gap: 16px; }
.card-grid { gap: 14px; }
.page > section > .sub { margin-bottom: 18px; }
.section-head { margin-bottom: 16px; }


/* A row of text cards shares a height — cards that each stop at a different
   point read as broken rather than as a set. A panel holding a chart is the
   exception: stretching a short chart to match a tall neighbour leaves a
   visible void under it, so those size to their own content instead. */
.split-2 > .panel,
.card-grid > .panel:has([id^="chart-"]) { align-self: start; }
/* Two chart pairs share a height rather than each sizing to its own content:
   the grant pair (the year chart is capped to six rows so they are close
   anyway) and the output/type pair (the bar chart grows to fill). In both the
   panel is a flex column and the chart takes the slack, so nothing ends in a
   void. */
.split-2:has(#chart-funders) > .panel,
.split-2:has(#chart-years) > .panel { align-self: stretch; display: flex; flex-direction: column; }
.split-2:has(#chart-funders) > .panel > div[id^="chart-"],
.split-2:has(#chart-years) > .panel > div[id^="chart-"] { flex: 1; }
#chart-years { display: flex; flex-direction: column; }

/* A cut-out PNG has no rectangle to hide, so it drops the vignette and
   the background tint and keeps only the bottom fade that sinks the
   torso into the network. This is what produces the "standing in the
   data" look; the masked JPG above is the graceful fallback. */
.portrait-media.has-cutout {
  /* Bottom fade sinks the torso into the network. The narrow left/right
     fade softens the point where the subject meets the edge of the
     source photograph — most head-and-shoulders shots are cropped at
     the shoulder, and without this that crop reads as a hard cut. */
  -webkit-mask-image:
    linear-gradient(to bottom, #000 0%, #000 62%, rgba(0,0,0,.86) 78%,
                    rgba(0,0,0,.4) 90%, transparent 100%),
    linear-gradient(to right, transparent 0%, rgba(0,0,0,.7) 3%, #000 7%,
                    #000 93%, rgba(0,0,0,.7) 97%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to bottom, #000 0%, #000 62%, rgba(0,0,0,.86) 78%,
                    rgba(0,0,0,.4) 90%, transparent 100%),
    linear-gradient(to right, transparent 0%, rgba(0,0,0,.7) 3%, #000 7%,
                    #000 93%, rgba(0,0,0,.7) 97%, transparent 100%);
  mask-composite: intersect;
}
.portrait-media.has-cutout::after { display: none; }
.portrait-media.has-cutout img {
  object-fit: contain; object-position: 50% 100%;
  filter: drop-shadow(0 18px 40px color-mix(in srgb, var(--accent) 22%, transparent));
}


/* ============================================================
   Journal lists: analytics panel, facets, detail modal
   ============================================================ */
.jpanel { display: grid; gap: 16px; }

.jstats {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.jstat {
  padding: 18px 20px; border-radius: 18px;
  border: 1px solid var(--brd); background: var(--panel); backdrop-filter: blur(16px);
}
.jstat b {
  display: block; font-size: 27px; font-weight: 800; letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(120deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.jstat span {
  display: block; margin-top: 4px; font-size: 11px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--fg-2);
}
.jstat em { display: block; margin-top: 3px; font-size: 12px; font-style: normal; color: var(--fg-3); }

/* Three columns of similar height. The two quartile splits used to be
   separate short cards next to a tall one, which left a large empty
   band across the row. */
.jcharts {
  display: grid; gap: 14px; align-items: stretch;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
}
.jchart.is-pair { display: grid; gap: 22px; align-content: start; }
.jchart {
  padding: 20px 22px; border-radius: 18px;
  border: 1px solid var(--brd); background: var(--panel); backdrop-filter: blur(16px);
}
.jchart h2, .jchart h4 {
  font-size: 13px; font-weight: 700; margin: 0 0 14px;
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.jchart h2 span, .jchart h4 span {
  font-family: "JetBrains Mono", monospace; font-size: 11px;
  font-weight: 500; color: var(--fg-3);
}

/* quartile split bar */
.qsplit { display: flex; height: 10px; border-radius: 999px; overflow: hidden; gap: 2px; }
.qseg { display: block; min-width: 2px; }
.qseg.q1 { background: #22c55e; }
.qseg.q2 { background: #38bdf8; }
.qseg.q3 { background: #fbbf24; }
.qseg.q4 { background: #f472b6; }
.qkey { display: flex; flex-wrap: wrap; gap: 8px 11px; margin-top: 12px; font-size: 11.5px; color: var(--fg-2); }
.qkey span { display: inline-flex; align-items: center; gap: 6px; }
.qkey i { width: 8px; height: 8px; border-radius: 3px; }
.qkey i.q1 { background: #22c55e; } .qkey i.q2 { background: #38bdf8; }
.qkey i.q3 { background: #fbbf24; } .qkey i.q4 { background: #f472b6; }
.qkey b { font-variant-numeric: tabular-nums; color: var(--fg); }

/* clickable ranked bars */
.jbars { display: grid; gap: 8px; }
.jbar {
  display: grid; grid-template-columns: minmax(0, 1fr) 88px auto; gap: 10px;
  align-items: center; width: 100%; padding: 4px 6px; border-radius: 9px;
  background: none; border: 0; cursor: pointer; font: inherit; text-align: left;
  transition: background .18s ease;
}
.jbar:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
/* a distribution band stays lit while it is the active filter, so the chart
   doubles as the read-out of what you clicked */
.jbar.is-on { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.jbar-label {
  font-size: 12.5px; color: var(--fg-2); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.jbar:hover .jbar-label,
.jbar.is-on .jbar-label { color: var(--fg); }
.jbar-label.mono {
  font-family: "JetBrains Mono", monospace; font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}
.jbar-track {
  height: 6px; border-radius: 999px;
  background: color-mix(in srgb, var(--fg) 10%, transparent); overflow: hidden;
}
.jbar-track i {
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
}
.jbar-n {
  font-family: "JetBrains Mono", monospace; font-size: 11.5px;
  color: var(--fg-3); font-variant-numeric: tabular-nums;
}

/* facets */
.jfacets, .jfacet-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; }
.jfacets { margin: 14px 0 4px; }
.jfacet-controls { display: contents; }
.jfgroup { display: inline-flex; align-items: center; gap: 6px; }
.jflabel {
  font-size: 10.5px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--fg-3); margin-right: 2px;
}
.jchip {
  padding: 6px 13px; border-radius: 999px; font-size: 12.5px; font-weight: 600;
  border: 1px solid var(--brd); background: var(--panel); color: var(--fg-2);
  cursor: pointer; font-family: inherit; transition: all .18s ease;
}
.jchip:hover { color: var(--fg); border-color: var(--accent); }
.jchip.is-on { color: var(--bg); background: var(--fg); border-color: var(--fg); }
.jchip.q-q1.is-on { background: #22c55e; border-color: #22c55e; color: #04210f; }
.jchip.q-q2.is-on { background: #38bdf8; border-color: #38bdf8; color: #04202b; }
.jchip.q-q3.is-on { background: #fbbf24; border-color: #fbbf24; color: #2a1c00; }
.jchip.q-q4.is-on { background: #f472b6; border-color: #f472b6; color: #2c0619; }
.jgroup-filter { display: inline-flex; align-items: center; gap: 8px; }
.jselect { display: inline-flex; align-items: center; gap: 8px; }
.jselect span {
  font-size: 10.5px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--fg-3);
}
.jgroup-search {
  width: min(290px, 46vw); min-width: 190px; padding: 7px 31px 7px 11px;
  border: 1px solid var(--brd); border-radius: 10px; background: var(--panel);
  color: var(--fg); font-family: inherit; font-size: 13px;
}
.jgroup-search:hover { border-color: var(--fg-3); }
.jgroup-search:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-color: var(--accent); }
.jclear {
  padding: 6px 13px; border-radius: 999px; font-size: 12.5px; font-weight: 600;
  border: 1px dashed color-mix(in srgb, var(--accent) 45%, transparent);
  background: none; color: var(--accent); cursor: pointer; font-family: inherit;
}
.jclear:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }

/* rows are controls now */
.jtable tbody tr[data-idx] { cursor: pointer; }
.jtable tbody tr[data-idx]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}

/* ── detail modal ─────────────────────────────────────────── */
body.modal-open { overflow: hidden; }
.jmodal { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 24px; }
.jmodal-back {
  position: absolute; inset: 0;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: blur(8px);
  opacity: 0; transition: opacity .22s ease;
}
.jmodal.is-open .jmodal-back { opacity: 1; }
.jmodal-card {
  --tone: var(--accent);
  position: relative; width: min(920px, 100%); max-height: min(88vh, 720px);
  overflow-y: auto; padding: 0; border-radius: 26px; overflow-x: hidden;
  border: 1px solid color-mix(in srgb, var(--tone) 32%, var(--brd));
  background: var(--panel);
  backdrop-filter: blur(24px);
  box-shadow: 0 40px 100px rgba(0,0,0,.45),
              0 0 0 1px color-mix(in srgb, var(--tone) 12%, transparent);
  opacity: 0; transform: translateY(14px) scale(.985);
  transition: opacity .26s cubic-bezier(.22,1,.36,1), transform .26s cubic-bezier(.22,1,.36,1);
}
.jmodal.is-open .jmodal-card { opacity: 1; transform: none; }
.jmodal-card:focus { outline: none; }
.jmodal-x {
  /* The header is positioned and comes later in the DOM, so with z-index
     auto it painted over this button and swallowed the click. */
  position: absolute; top: 18px; right: 18px; z-index: 2;
  width: 34px; height: 34px;
  display: grid; place-items: center; border-radius: 50%;
  border: 1px solid var(--brd); background: var(--bg); color: var(--fg-2);
  font-size: 19px; line-height: 1; cursor: pointer;
}
.jmodal-x:hover { color: var(--fg); border-color: var(--accent); }
.jmodal-head { padding-right: 44px; margin-bottom: 20px; }
.jmodal-head h3 { font-size: 24px; font-weight: 800; letter-spacing: -.02em; margin: 6px 0 0; line-height: 1.25; }
.jmodal-head p { font-size: 13.5px; color: var(--fg-3); margin: 8px 0 0; }

.jbadges { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
/* the publication modal reuses the journal shell, so its head badges are the
   same quartile and rating chips the journal lists use */
.jbadges .q, .jbadges .rate { font-size: 11px; }
.jbadges .q.oa-badge {
  background: color-mix(in srgb, #22c55e 20%, transparent); color: #4ade80;
}
html[data-theme="light"] .jbadges .q.oa-badge { color: #15803d; }
.jmodal-head .venue-line { color: var(--fg-2); font-style: italic; margin-top: 4px; }
/* an abstract is the point of the record, so it is not clamped here */
.jbrief.is-full { -webkit-line-clamp: unset; display: block; overflow: visible; }
.jmodal.is-pub .jmodal-cols { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); }
/* a full abstract plus the venue's metrics is a taller record than a journal's,
   so it gets more room before it starts scrolling */
.jmodal.is-pub .jmodal-card { max-height: min(90vh, 900px); }
.jmodal.is-pub .pub-cites-split { margin-top: 18px; }
.jmodal.is-pub .jtiles { margin-bottom: 4px; }
/* the abstract follows the figures, so it needs the rule above it */
.jmodal.is-pub .jbrief.is-full {
  margin: 20px 0 0; padding-top: 18px; border-top: 1px solid var(--brd-2);
}
.jbadge {
  padding: 4px 11px; border-radius: 999px; font-size: 11.5px; font-weight: 700;
  letter-spacing: .03em;
}
.jbadge.is-oa { background: color-mix(in srgb, #22c55e 18%, transparent); color: #4ade80; }
.jbadge.is-warn { background: color-mix(in srgb, #f472b6 18%, transparent); color: #f472b6; }
html[data-theme="light"] .jbadge.is-oa { color: #15803d; }

.jbrief { font-size: 14.5px; line-height: 1.75; color: var(--fg-2); margin: 0 0 20px; }

.jtiles {
  display: grid; gap: 10px; margin-bottom: 20px;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
}
.jtile {
  padding: 14px 12px; border-radius: 14px; text-align: center;
  border: 1px solid var(--brd-2);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}
.jtile b {
  display: block; font-size: 20px; font-weight: 800; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; color: var(--fg);
}
/* Scoped tightly, for two separate reasons.

   `> ... :not([popover])`: a bare `.jtile span` also matched the source panel,
   which is a <span> in the tile, and the source label inside the attribution
   button. An author rule beats the UA stylesheet whatever the specificity, so
   `display: block` here overrode the UA rule that hides a *closed* popover and
   the panel sat on screen permanently — fixed-position, never in the top
   layer, so nothing could dismiss it.

   No `text-transform`: these captions are proper names, and uppercasing them
   was simply wrong. "CiteScore" is how Elsevier writes it; the same rule
   reaching the label below turned MyCite into MYCITE and SCImago into SCIMAGO.
   Every label here is already written in the case it should appear in, so the
   transform only ever did damage. The acronyms are unaffected. */
.jtile > span:not([popover]) {
  display: block; margin-top: 3px; font-size: 10px; font-weight: 700;
  letter-spacing: .06em; color: var(--fg-3);
}

.jpairs {
  display: grid; grid-template-columns: auto 1fr; gap: 10px 20px; margin: 0 0 22px;
  font-size: 13.5px;
}
.jpairs dt { color: var(--fg-3); }
.jpairs dd { margin: 0; color: var(--fg); }
.jactions { display: flex; gap: 10px; }

@media (max-width: 720px) {
  .jmodal { padding: 12px; }
  .jmodal-card { padding: 26px 22px; max-height: 90vh; }
  .jmodal-head h3 { font-size: 20px; }
  .jpairs { grid-template-columns: 1fr; gap: 3px 0; }
  .jpairs dd { margin-bottom: 10px; }
  .jbar { grid-template-columns: minmax(0, 1fr) 56px auto; }
  .jgroup-search { width: min(230px, 58vw); min-width: 150px; }
}
@media (prefers-reduced-motion: reduce) {
  .jmodal-card, .jmodal-back { transition: none; }
}


/* ── modal: colour, two-column body, no scrolling ─────────── */
.jmodal-head {
  position: relative; padding: 30px 56px 22px 34px; margin: 0;
  background:
    radial-gradient(120% 180% at 0% 0%,
      color-mix(in srgb, var(--tone) 26%, transparent), transparent 62%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--tone) 12%, transparent), transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--tone) 22%, var(--brd-2));
}
.jmodal-head::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: linear-gradient(90deg, var(--tone), color-mix(in srgb, var(--tone) 20%, transparent));
}
.jmodal-head .kicker { color: var(--tone); }
.jmodal-head h3 { font-size: 25px; font-weight: 800; letter-spacing: -.02em; margin: 6px 0 0; line-height: 1.22; }
.jmodal-head p { font-size: 13.5px; color: var(--fg-2); margin: 8px 0 0; }
.jmodal-head .jbadges { margin: 14px 0 0; }

.jmodal-body { padding: 26px 34px 30px; }
.jmodal-cols {
  display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: 30px; align-items: start;
}
.jbrief {
  font-size: 14px; line-height: 1.7; color: var(--fg-2); margin: 0 0 18px;
  display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden;
}
.jbrief.is-muted { color: var(--fg-3); font-style: italic; }

.jtiles { grid-template-columns: repeat(auto-fit, minmax(84px, 1fr)); margin: 0; gap: 8px; }
.jtile {
  padding: 12px 8px; border-radius: 13px;
  border: 1px solid var(--brd-2);
  background: color-mix(in srgb, var(--tone) 8%, transparent);
}
.jtile b { font-size: 18px; }
.jtile.is-apc {
  border-color: color-mix(in srgb, var(--tone) 45%, transparent);
  background: color-mix(in srgb, var(--tone) 17%, transparent);
}
.jtile.is-apc b { color: var(--tone); }
.jtile.is-free b { color: #22c55e; }
html[data-theme="light"] .jtile.is-free b { color: #15803d; }

.jmodal-right .jpairs {
  grid-template-columns: auto 1fr; gap: 9px 18px; margin: 0; font-size: 13px;
}
/* Six link buttons will not sit on one line on a laptop, so the row wraps and
   Close is pushed to the far end where it stays findable. */
.jactions {
  display: flex; flex-wrap: wrap; gap: 10px; margin: 26px 34px 30px; padding-top: 20px;
  border-top: 1px solid var(--brd-2);
}
.jactions .btn { font-size: 13px; padding: 9px 15px; }
.jactions [data-close] { margin-left: auto; }
.jmodal-body + .jactions { margin-top: 0; }

/* A description assembled from OpenAlex facts rather than written by hand —
   marked so it does not read as the journal's own words. */
.jbrief.is-derived { -webkit-line-clamp: 6; }
.jbrief.is-derived::after {
  /* "Description assembled from OpenAlex", not "from OpenAlex". The short
     form sat between the paragraph it belongs to and the metric tiles below,
     where it read as a caption for the tiles — which are Scopus and Clarivate
     figures. Naming what it describes removes the ambiguity. */
  content: "Description assembled from OpenAlex data"; display: block; margin-top: 6px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--fg-3);
}

/* Output per year. Bars only — the shape is the message, and an axis of
   sixteen year labels would crowd it out. Each bar carries its own tooltip. */
.jtimeline { margin-top: 18px; }
.jtimeline h5 {
  margin: 0 0 9px; font-size: 10.5px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--fg-3);
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
}
.jtimeline h5 span {
  font-family: "JetBrains Mono", monospace; font-size: 10.5px;
  letter-spacing: 0; text-transform: none; font-weight: 500;
}
.spark {
  display: flex; align-items: stretch; gap: 3px; height: 64px;
  padding: 0 2px; border-radius: 8px;
}
/* Each year is a column the bar is anchored to the bottom of, with its figure
   floating just above it. Both are positioned as a percentage of the column,
   so the label tracks the bar's height instead of sitting on a fixed row. */
.spark-col { position: relative; flex: 1 1 0; min-width: 3px; }
.spark-bar {
  position: absolute; left: 0; right: 0; bottom: 0;
  border-radius: 3px 3px 1px 1px;
  background: linear-gradient(180deg, var(--accent), var(--accent-2));
  transition: opacity .15s ease;
}
.spark-col em {
  position: absolute; left: -2px; right: -2px; text-align: center;
  font-family: "JetBrains Mono", monospace; font-size: 9.5px; font-weight: 500;
  font-style: normal; line-height: 1; white-space: nowrap;
  color: var(--fg-3); transition: color .15s ease;
}
/* Past fourteen columns each is under ~30px and the numbers collide. The bars
   still read as a shape, and `title` still carries the exact figure. */
.spark.is-dense { height: 46px; }
.spark.is-dense .spark-col em { display: none; }
.spark-col:hover .spark-bar { opacity: .65; }
.spark-col:hover em { color: var(--accent); }
/* the current year is not over, so its bar is drawn hollow rather than read
   as a collapse in output */
.spark-bar.is-partial {
  background: none; border: 1px dashed color-mix(in srgb, var(--accent) 55%, transparent);
}
.spark-axis {
  display: flex; justify-content: space-between; margin-top: 5px;
  font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--fg-3);
}

@media (max-width: 980px) {
  .blog-layout { grid-template-columns: minmax(0, 1fr); }
  .blog-rail { position: static; }
}

@media (max-width: 780px) {
  .jmodal-card { max-height: 90vh; }
  .jmodal-head { padding: 24px 50px 18px 22px; }
  .jmodal-head h3 { font-size: 20px; }
  .jmodal-body { padding: 20px 22px 24px; }
  .jmodal-cols { grid-template-columns: 1fr; gap: 20px; }
  .jactions { margin: 0 22px 24px; }
}

/* ── ABDC / AJG rating badges ─────────────────────────────── */
.rate {
  display: inline-block; padding: 3px 9px; border-radius: 7px;
  font-family: "JetBrains Mono", monospace; font-size: 11.5px; font-weight: 700;
  letter-spacing: .01em; white-space: nowrap;
}
.rate.r-1 { background: color-mix(in srgb, #22c55e 24%, transparent); color: #4ade80; }
.rate.r-2 { background: color-mix(in srgb, #38bdf8 24%, transparent); color: #7dd3fc; }
.rate.r-3 { background: color-mix(in srgb, #fbbf24 24%, transparent); color: #fcd34d; }
.rate.r-4 { background: color-mix(in srgb, #fb923c 24%, transparent); color: #fdba74; }
.rate.r-5 { background: color-mix(in srgb, #f472b6 22%, transparent); color: #f9a8d4; }
.rate.r-0 { background: color-mix(in srgb, var(--fg) 12%, transparent); color: var(--fg-2); }
html[data-theme="light"] .rate.r-1 { color: #15803d; }
html[data-theme="light"] .rate.r-2 { color: #0369a1; }
html[data-theme="light"] .rate.r-3 { color: #a16207; }
html[data-theme="light"] .rate.r-4 { color: #c2410c; }
html[data-theme="light"] .rate.r-5 { color: #be185d; }

/* rating chips reuse the same scale when active */
.jchip.rate-chip.is-on.r-1 { background: #22c55e; border-color: #22c55e; color: #04210f; }
.jchip.rate-chip.is-on.r-2 { background: #38bdf8; border-color: #38bdf8; color: #04202b; }
.jchip.rate-chip.is-on.r-3 { background: #fbbf24; border-color: #fbbf24; color: #2a1c00; }
.jchip.rate-chip.is-on.r-4 { background: #fb923c; border-color: #fb923c; color: #2b1200; }
.jchip.rate-chip.is-on.r-5 { background: #f472b6; border-color: #f472b6; color: #2c0619; }
.jbar-label .rate { font-size: 11px; }

/* What a ratings review did to a journal. Not an ordinal scale, so it does not
   borrow the A*–C ramp: direction is the meaning, and the colours say it —
   green up, amber down, pink gone, neutral for the ones that did not move. */
.change {
  display: inline-block; padding: 3px 9px; border-radius: 7px;
  font-size: 11px; font-weight: 700; letter-spacing: .01em; white-space: nowrap;
}
.change.c-new,
.change.c-upgraded  { background: color-mix(in srgb, #22c55e 22%, transparent); color: #4ade80; }
.change.c-downgraded{ background: color-mix(in srgb, #fb923c 24%, transparent); color: #fdba74; }
.change.c-delisted  { background: color-mix(in srgb, #f472b6 22%, transparent); color: #f9a8d4; }
.change.c-unchanged { background: color-mix(in srgb, var(--fg) 10%, transparent); color: var(--fg-3); }
html[data-theme="light"] .change.c-new,
html[data-theme="light"] .change.c-upgraded   { color: #15803d; }
html[data-theme="light"] .change.c-downgraded { color: #c2410c; }
html[data-theme="light"] .change.c-delisted   { color: #be185d; }
.jbar-label .change { font-size: 11px; }
.jchip.change-chip.is-on.c-new,
.jchip.change-chip.is-on.c-upgraded   { background: #22c55e; border-color: #22c55e; color: #04210f; }
.jchip.change-chip.is-on.c-downgraded { background: #fb923c; border-color: #fb923c; color: #2b1200; }
.jchip.change-chip.is-on.c-delisted   { background: #f472b6; border-color: #f472b6; color: #2c0619; }

.workshop-past-head {
  margin: 34px 0 14px; font-size: 11px; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--fg-3);
}
.workshop.is-past { opacity: .68; }
.workshop.is-past:hover { opacity: 1; }

/* ── curriculum vitae ──────────────────────────────────────────
   One set of styles serves the screen and the printer. The screen version
   sits on the site's dark panel; @media print at the end turns it into a
   white A4 document. There is no separate print build to keep in sync — the
   thing you read is the thing that prints.
   ──────────────────────────────────────────────────────────── */
.cv-actions {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  max-width: 900px; margin: 0 auto 20px;
}
.cv-actions .btn { font-size: 13px; padding: 9px 15px; }
.cv-actions [data-print] { margin-left: auto; }

.cv {
  max-width: 900px; margin: 0 auto 60px; padding: clamp(28px, 4vw, 54px);
  border-radius: 22px; border: 1px solid var(--brd); background: var(--panel);
  backdrop-filter: blur(16px);
  font-size: 14px; line-height: 1.65; color: var(--fg-2);
}

.cv-head { padding-bottom: 20px; border-bottom: 2px solid var(--accent); }
.cv-head h1 {
  font-size: clamp(26px, 3.4vw, 36px); font-weight: 800; letter-spacing: -.02em;
  margin: 0; color: var(--fg);
}
.cv-title { margin: 6px 0 0; font-size: 15px; font-weight: 600; color: var(--accent); }
.cv-org { margin: 3px 0 0; font-size: 13.5px; }
.cv-contact { margin: 10px 0 0; font-size: 12px; color: var(--fg-3); }
.cv-web { font-family: "JetBrains Mono", monospace; font-size: 11px; }

/* the headline numbers, so the first thing read is the shape of the record */
.cv-facts {
  display: grid; gap: 10px; margin: 22px 0 6px;
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
}
.cv-facts div {
  padding: 12px 6px; border-radius: 12px; text-align: center;
  border: 1px solid var(--brd-2);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}
.cv-facts b { display: block; font-size: 19px; font-weight: 800; color: var(--fg); }
.cv-facts span {
  display: block; margin-top: 2px; font-size: 9.5px; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase; color: var(--fg-3);
}

.cv-section { margin-top: 26px; }
.cv-section h2 {
  margin: 0 0 12px; font-size: 11px; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; color: var(--accent);
  padding-bottom: 7px; border-bottom: 1px solid var(--brd-2);
}
.cv-section p { margin: 0 0 9px; }

/* a date column and a content column: the eye scans the left edge for when,
   then reads across for what */
.cv-rows { display: grid; gap: 13px; }
.cv-row { display: grid; grid-template-columns: 128px minmax(0, 1fr); gap: 16px; }
.cv-when {
  font-family: "JetBrains Mono", monospace; font-size: 11.5px; color: var(--fg-3);
  padding-top: 2px;
}
.cv-amt { display: block; margin-top: 3px; color: var(--accent); }
.cv-what b { color: var(--fg); font-weight: 700; }
.cv-sub { color: var(--fg-3); font-size: 12.5px; }
.cv-tag {
  display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 999px;
  font-size: 10px; font-weight: 700; letter-spacing: .04em;
  background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent);
}

.cv-pubs { margin: 0; padding-left: 20px; display: grid; gap: 11px; }
.cv-pub { display: grid; grid-template-columns: minmax(0, 1fr) 46px; gap: 12px; align-items: start; }
.cv-pub em { color: var(--fg); font-style: normal; font-weight: 600; }
.cv-doi { font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--fg-3); }
.cv-marks {
  display: inline-block; margin-left: 4px; font-size: 10.5px; font-weight: 700;
  letter-spacing: .03em; color: var(--accent);
}
.cv-cites {
  font-family: "JetBrains Mono", monospace; font-size: 11.5px; text-align: right;
  color: var(--fg-3); padding-top: 1px;
}
.cv-cites::after { content: " cit."; font-size: 9px; }

.cv-list { margin: 0; padding-left: 20px; display: grid; gap: 7px; }
.cv-list b { color: var(--fg); }
.cv-note { margin-top: 10px; font-size: 12px; color: var(--fg-3); font-style: italic; }
.cv-foot {
  margin-top: 30px; padding-top: 14px; border-top: 1px solid var(--brd-2);
  font-size: 10.5px; letter-spacing: .04em; color: var(--fg-3); text-align: center;
}

@media (max-width: 620px) {
  .cv-row { grid-template-columns: minmax(0, 1fr); gap: 3px; }
  .cv-pub { grid-template-columns: minmax(0, 1fr); }
  .cv-cites { text-align: left; }
}

/* ── print ─────────────────────────────────────────────────────
   Cmd-P on any CV route produces the PDF. Everything that is chrome goes,
   the page turns white, and entries are kept off page boundaries — a grant
   split across two sheets is the thing that makes a generated CV look
   generated.
   ──────────────────────────────────────────────────────────── */
@media print {
  @page { size: A4; margin: 14mm 13mm; }

  .nav, .footer, .bg-aurora, .bg-grid, .no-print, .qr-overlay, #j-modal { display: none !important; }
  html, body {
    background: #fff !important; color: #111 !important;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  body { font-size: 10pt; }
  .page { display: block !important; padding: 0 !important; margin: 0 !important; }
  .page[hidden] { display: none !important; }

  .cv {
    max-width: none; margin: 0; padding: 0; border: 0; border-radius: 0;
    background: none; backdrop-filter: none; box-shadow: none;
    font-size: 9.4pt; line-height: 1.45; color: #1a1a1a;
  }
  .cv-head { border-bottom: 1.5pt solid #1a1a1a; padding-bottom: 10pt; }
  .cv-head h1 { font-size: 19pt; color: #000; }
  .cv-title { color: #000; font-size: 10.5pt; margin-top: 3pt; }
  .cv-org, .cv-contact { color: #444; }
  .cv-contact { font-size: 8pt; margin-top: 6pt; }

  /* auto-fit left seven tiles as six-plus-one orphan and wrapped the money
     figure inside its box; four across gives 4 + 3 and room for the number */
  .cv-facts { gap: 5pt; margin: 12pt 0 0; grid-template-columns: repeat(4, 1fr); }
  .cv-one .cv-facts { grid-template-columns: repeat(5, 1fr); }
  .cv-facts div { border: 0.5pt solid #bbb; background: #f6f6f6; padding: 6pt 3pt; }
  .cv-facts b { font-size: 12pt; color: #000; }
  .cv-facts span { color: #555; font-size: 6.5pt; }

  .cv-section { margin-top: 14pt; break-inside: auto; }
  .cv-section h2 {
    font-size: 8.5pt; color: #000; border-bottom: 0.5pt solid #999;
    margin-bottom: 7pt; padding-bottom: 3pt;
    break-after: avoid; page-break-after: avoid;
  }
  /* Chrome fragments a CSS grid container badly: break-inside on a grid ITEM
     is ignored when the container itself is the thing spanning pages, so the
     lists become plain block flow for print and only the individual entry
     stays a grid. Spacing moves from `gap` to margins to match. */
  .cv-rows, .cv-pubs, .cv-list { display: block; }
  .cv-rows > * + * { margin-top: 7pt; }
  .cv-pubs > * + * { margin-top: 6pt; }
  .cv-list > * + * { margin-top: 4pt; }
  /* an entry that straddles a page break is what makes a CV look generated */
  .cv-row, .cv-pub, .cv-list li { break-inside: avoid; page-break-inside: avoid; }
  .cv-row { grid-template-columns: 92pt minmax(0, 1fr); gap: 10pt; }
  .cv-when { font-size: 8pt; color: #555; }
  .cv-amt, .cv-marks, .cv-tag { color: #000; background: none; }
  .cv-tag { border: 0.5pt solid #999; padding: 0 4pt; }
  .cv-what b, .cv-pub em, .cv-list b, .cv-facts b { color: #000; }
  .cv-sub, .cv-doi, .cv-cites, .cv-note { color: #555; }
  /* "284 cit." was wrapping onto two lines in a 46px column */
  .cv-pub { grid-template-columns: minmax(0, 1fr) 34pt; gap: 8pt; }
  .cv-cites { font-size: 7.4pt; white-space: nowrap; }
  .cv-foot { border-top: 0.5pt solid #999; color: #666; margin-top: 16pt; }

  /* The one-pager is a fixed budget: one A4 side. Everything is a notch
     tighter than the longer CVs, and the content itself is shorter — four
     publications without DOIs, three grants, three awards. */
  .cv-one { font-size: 8.4pt; line-height: 1.35; }
  .cv-one .cv-head { padding-bottom: 6pt; }
  .cv-one .cv-head h1 { font-size: 15.5pt; }
  .cv-one .cv-title { font-size: 9pt; margin-top: 2pt; }
  .cv-one .cv-contact { margin-top: 4pt; font-size: 7.2pt; }
  .cv-one .cv-facts { margin-top: 7pt; gap: 4pt; }
  .cv-one .cv-facts div { padding: 4pt 2pt; }
  .cv-one .cv-facts b { font-size: 10.5pt; }
  .cv-one .cv-facts span { font-size: 5.8pt; }
  .cv-one .cv-section { margin-top: 6.4pt; }
  .cv-one .cv-section h2 { font-size: 7.6pt; margin-bottom: 4pt; padding-bottom: 2pt; }
  .cv-one .cv-rows > * + * { margin-top: 4.1pt; }
  .cv-one .cv-row { grid-template-columns: 76pt minmax(0, 1fr); gap: 8pt; }
  .cv-one .cv-when { font-size: 7.2pt; }
  .cv-one .cv-pubs { padding-left: 13pt; }
  .cv-one .cv-pubs > * + * { margin-top: 4.1pt; }
  .cv-one .cv-foot { margin-top: 7pt; padding-top: 4pt; font-size: 6.8pt; }
  .cv-one .cv-note { margin-top: 4pt; font-size: 7pt; }

  a { color: inherit !important; text-decoration: none; }
}

/* the CV offer on the About page */
.cv-picker { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.cv-card {
  display: flex; flex-direction: column; gap: 8px; padding: 24px;
  border-radius: 20px; border: 1px solid var(--brd); background: var(--panel);
  backdrop-filter: blur(16px); color: inherit;
  transition: transform .28s cubic-bezier(.22,1,.36,1), border-color .28s ease;
}
.cv-card:hover { transform: translateY(-4px); border-color: var(--accent); color: inherit; }
.cv-card-len {
  font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--accent);
  letter-spacing: .04em;
}
.cv-card h3 { margin: 0; font-size: 19px; font-weight: 700; }
.cv-card p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--fg-2); }
.cv-card-go { margin-top: auto; padding-top: 8px; font-size: 13px; font-weight: 600; color: var(--accent); }

/* "there is more of this on the CV" — used where the About page shows a
   deliberate subset rather than the whole list. It is appended inside the grid
   it describes, so it has to span every column instead of taking a cell. */
.tl-more { margin: 14px 0 0; font-size: 13px; font-weight: 600; grid-column: 1 / -1; }

/* ── analytics: combined output + citations chart ─────────────
   The citation curve is drawn over the year bars rather than beside them, so
   the two series share one x-axis instead of asking the reader to hold two.
   ──────────────────────────────────────────────────────────── */
.bars { position: relative; }
.bars-overlay {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; overflow: visible;
}
.chart-legend {
  display: flex; flex-wrap: wrap; gap: 8px 20px; margin-top: 14px;
  font-size: 12px; color: var(--fg-3);
}
.chart-legend span { display: inline-flex; align-items: center; gap: 7px; }
.chart-legend strong { color: var(--fg-2); font-variant-numeric: tabular-nums; }
.chart-legend i { display: inline-block; border-radius: 2px; }
.chart-legend .k-bar {
  width: 9px; height: 12px;
  background: linear-gradient(180deg, var(--accent), var(--accent-2));
}
.chart-legend .k-line { width: 16px; height: 2.5px; background: var(--accent-3); }

/* the quartile split under the type donut */
.donut-quart { margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--brd-2); }
.donut-quart h3, .donut-quart h4 {
  margin: 0 0 10px; font-size: 10.5px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--fg-3);
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.donut-quart h3 span, .donut-quart h4 span {
  font-family: "JetBrains Mono", monospace; font-size: 10.5px;
  letter-spacing: 0; text-transform: none; font-weight: 500;
}

/* Six training areas read as two rows of three; auto-fit gave four then two,
   which left an orphan row. */
#training-areas { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 900px) { #training-areas { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) { #training-areas { grid-template-columns: minmax(0, 1fr); } }

/* Grant value by year and Where the funding comes from are a pair: the shorter
   one stretches to the taller and spreads its bars into the space, so the
   column does not end in a void. */
#chart-grants, #chart-funders { height: 100%; }
#chart-grants .hbars, #chart-funders .hbars {
  height: 100%; display: flex; flex-direction: column; justify-content: space-between;
}

/* ── tools ─────────────────────────────────────────────────────
   Browser-side utilities. Styled from the site's tokens rather than the
   standalone light-card the tool arrived as, so it belongs to the page and
   works in both themes.
   ──────────────────────────────────────────────────────────── */
.tool { display: grid; gap: 18px; }

/* `display` set in CSS beats the [hidden] attribute's own display:none, so
   hiding the drop zone from script needs saying explicitly */
.tool-drop[hidden] { display: none; }
.tool-drop {
  display: grid; place-items: center; gap: 6px; text-align: center;
  padding: clamp(34px, 6vw, 56px) 24px;
  border: 1.5px dashed var(--brd); border-radius: 20px;
  background: var(--panel); backdrop-filter: blur(16px);
  color: var(--fg-3); cursor: pointer;
  transition: border-color .2s ease, background .2s ease, color .2s ease;
}
.tool-drop:hover, .tool-drop.is-over, .tool-drop:focus-visible {
  border-color: var(--accent); color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--panel));
  outline: none;
}
.tool-drop strong { font-size: 16px; font-weight: 700; color: var(--fg); }
.tool-drop p { margin: 0; font-size: 13.5px; }
.tool-drop svg { opacity: .8; }

.tool-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px; }
.tool-row label { font-size: 13.5px; color: var(--fg-2); }
.tool-row input[type="number"] {
  width: 116px; padding: 10px 12px; border-radius: 12px;
  border: 1px solid var(--brd); background: var(--panel); color: var(--fg);
  font-family: "JetBrains Mono", monospace; font-size: 14px;
}
.tool-row input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.tool-stats {
  display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.tool-stat {
  padding: 18px; border-radius: 16px;
  border: 1px solid var(--brd-2); background: var(--panel);
}
.tool-stat b { display: block; font-size: 26px; font-weight: 800; letter-spacing: -.02em; }
.tool-stat span {
  display: block; margin-top: 3px; font-size: 10.5px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--fg-3);
}
.tool-stat.is-warn { border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }
.tool-stat.is-warn b { color: var(--accent-2); }

.tool-sheet { margin-top: 8px; }
.tool-sheet h4 {
  margin: 0 0 10px; font-size: 10.5px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--fg-3);
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.tool-sheet h4 span {
  font-family: "JetBrains Mono", monospace; font-size: 10.5px;
  letter-spacing: 0; text-transform: none; font-weight: 500;
}
.tool-table td, .tool-table th { cursor: default; }
.tool-table tbody tr:hover { background: none; }

.tool-msg { margin: 0; font-size: 13.5px; color: var(--fg-2); }
.tool-msg.is-good { color: #4ade80; }
.tool-msg.is-bad { color: var(--accent-2); }
html[data-theme="light"] .tool-msg.is-good { color: #15803d; }

.tool-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }

/* "Keep reading" at the foot of a post. Same rail styling as the blog index,
   because it is the same kind of list — but split into posts and resources so
   a link that leaves the site is not dressed as an article that stays on it. */
.note-related { margin-top: 8px; padding-top: 28px; border-top: 1px solid var(--brd-2); }
.note-related h2 { font-size: 22px; font-weight: 700; margin: 0 0 18px; }
/* there are only ever two groups, so say two — auto-fit built a four-track
   grid and left the pair stranded at opposite ends of it */
.related-cols {
  display: grid; gap: 24px 40px; align-items: start;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  max-width: 820px;
}
@media (max-width: 620px) { .related-cols { grid-template-columns: minmax(0, 1fr); } }
.related-cols .rail-group + .rail-group { margin-top: 0; padding-top: 0; border-top: 0; }

/* a numbered video path inside a note body */
.vlist { list-style: none; counter-reset: v; margin: 6px 0 0; padding: 0; display: grid; gap: 10px; }
.vlist li { counter-increment: v; }
.vlist a {
  display: grid; grid-template-columns: 46px minmax(0, 1fr) auto; align-items: center; gap: 16px;
  padding: 16px 20px; border-radius: 16px; color: inherit;
  border: 1px solid var(--brd); background: var(--panel);
  transition: transform .22s cubic-bezier(.22,1,.36,1), border-color .22s ease;
}
.vlist a:hover { transform: translateX(6px); border-color: var(--accent); color: inherit; }
.vnum::before {
  content: counter(v, decimal-leading-zero);
  font-family: "JetBrains Mono", monospace; font-size: 20px; font-weight: 700;
  color: var(--fg-3); transition: color .22s ease;
}
.vlist a:hover .vnum::before { color: var(--accent); }
.vtext b { display: block; font-size: 15px; font-weight: 700; line-height: 1.4; }
.vtext em { display: block; margin-top: 3px; font-size: 13px; font-style: normal; color: var(--fg-3); }
.vplay {
  display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%;
  border: 1px solid var(--brd); color: var(--fg-2); font-size: 12px;
  transition: background .22s ease, color .22s ease, border-color .22s ease;
}
.vlist a:hover .vplay {
  background: var(--accent); border-color: var(--accent); color: var(--bg);
}
@media (max-width: 560px) {
  .vlist a { grid-template-columns: 34px minmax(0, 1fr); gap: 12px; padding: 14px 16px; }
  .vplay { display: none; }
}

/* the way in for someone who does not have the passcode */
.gate-cta { margin: 16px 0 0; font-size: 13.5px; font-weight: 600; }

/* ── product-page blocks ───────────────────────────────────────
   A note that is a product rather than an article. The original of the
   Bibliometrics Hub used a different hue for each group of links — tools,
   store, templates, samples — and on a page that is mostly buttons that
   colour is how you find the block you want, so it is kept rather than
   flattened into one accent. Each card sets --t and everything inside
   follows it.
   ──────────────────────────────────────────────────────────── */
.nhero {
  margin: 8px 0 26px; padding: clamp(30px, 5vw, 48px) 30px; border-radius: 26px;
  text-align: center; position: relative; overflow: hidden;
  background:
    radial-gradient(120% 160% at 12% 0%, color-mix(in srgb, var(--accent) 55%, transparent), transparent 60%),
    radial-gradient(120% 160% at 100% 100%, color-mix(in srgb, var(--accent-2) 45%, transparent), transparent 62%),
    linear-gradient(140deg, #16182c, #0d0f1c);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
}
.nhero h2 {
  margin: 0; font-size: clamp(26px, 4vw, 38px); font-weight: 800;
  letter-spacing: -.02em; color: #fff;
}
.nhero p { margin: 10px auto 0; max-width: 52ch; color: rgba(255,255,255,.72); font-size: 15px; }

.ncards {
  display: grid; gap: 18px; margin: 24px 0;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
}
.ncard {
  --t: var(--accent);
  display: flex; flex-direction: column; gap: 4px; padding: 26px;
  border-radius: 22px; border: 1px solid var(--brd); background: var(--panel);
  backdrop-filter: blur(16px); border-top: 5px solid var(--t);
  position: relative;
}
.ncard[data-tone="blue"]   { --t: #38bdf8; }
.ncard[data-tone="green"]  { --t: #22c55e; }
.ncard[data-tone="amber"]  { --t: #f59e0b; }
.ncard[data-tone="violet"] { --t: #a78bfa; }
.ncard[data-tone="pink"]   { --t: #ec4899; }
.ncard[data-tone="red"]    { --t: #ef4444; }
.ncard-badge {
  position: absolute; top: 20px; right: 22px;
  font-family: "JetBrains Mono", monospace; font-size: 10.5px; font-weight: 700;
  padding: 4px 10px; border-radius: 999px;
  background: color-mix(in srgb, var(--t) 18%, transparent); color: var(--t);
}
.ncard h3 { margin: 0; font-size: 19px; font-weight: 800; color: var(--t); }
.ncard p { margin: 4px 0 12px; font-size: 13.5px; line-height: 1.6; color: var(--fg-3); }
.ncard-links { display: grid; gap: 8px; }
/* A lone card spans the row rather than sitting in one narrow track with
   nothing beside it. With a single action it becomes a row: what it is on the
   left, the button on the right, no dead space in between. */
/* The banner's own calls to action. They sit inside `.nhero`, which is a
   gradient panel, so the ghost button needs a solid surface of its own rather
   than the translucent one it uses on the page background. */
.nhero-links {
  display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 20px;
}
.nhero-links .btn { font-size: 14px; }
.nhero-links .btn-ghost {
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  border-color: color-mix(in srgb, var(--fg) 26%, transparent);
}

.ncards:has(.ncard:only-child) { grid-template-columns: minmax(0, 1fr); }
.ncard:only-child { grid-column: 1 / -1; }
.ncard:only-child:has(.ncard-links > :only-child) {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 10px 28px;
}
/* in the row layout the top-right corner belongs to the button, so the badge
   moves up beside the heading instead of sitting on top of it */
.ncard:only-child:has(.ncard-links > :only-child) h3 {
  grid-column: 1; display: flex; align-items: center; gap: 12px;
}
.ncard:only-child:has(.ncard-links > :only-child) .ncard-badge {
  position: static; order: 2; align-self: center;
}
.ncard:only-child:has(.ncard-links > :only-child) p { grid-column: 1; margin-bottom: 0; }
.ncard:only-child:has(.ncard-links > :only-child) .ncard-links {
  grid-column: 2; grid-row: 1 / span 2; align-self: center;
}
.ncard:only-child:has(.ncard-links > :only-child) .nbtn { padding-inline: 30px; }
@media (max-width: 640px) {
  .ncard:only-child:has(.ncard-links > :only-child) { grid-template-columns: minmax(0, 1fr); }
  .ncard:only-child:has(.ncard-links > :only-child) .ncard-links { grid-column: 1; grid-row: auto; }
}

.nbtn {
  display: block; text-align: center; padding: 11px 14px; border-radius: 12px;
  font-size: 13.5px; font-weight: 700; color: var(--fg-2);
  border: 1px solid var(--brd-2); background: color-mix(in srgb, var(--fg) 4%, transparent);
  transition: transform .18s ease, background .18s ease, color .18s ease, border-color .18s ease;
}
.nbtn:hover {
  transform: translateY(-2px); color: var(--fg);
  border-color: color-mix(in srgb, var(--t) 55%, transparent);
  background: color-mix(in srgb, var(--t) 14%, transparent);
}
/* the one action the card exists for */
.nbtn.is-lead {
  color: #0b0d16; border-color: transparent;
  background: linear-gradient(135deg, var(--t), color-mix(in srgb, var(--t) 60%, #fff));
}
.nbtn.is-lead:hover { color: #0b0d16; filter: brightness(1.08); }
html[data-theme="light"] .nbtn.is-lead { color: #fff; }
/* two short links sit side by side rather than as two full-width bars */
.ncard-links.is-pairs { grid-template-columns: 1fr 1fr; }

.vhead {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  margin: 26px 0 12px;
}
.vhead h3 { margin: 0; font-size: 19px; font-weight: 800; }
.vhead span {
  font-family: "JetBrains Mono", monospace; font-size: 11px; font-weight: 700;
  padding: 5px 12px; border-radius: 999px;
  /* red rather than the magenta this used to be: every one of these is a
     YouTube link, and red is what a play control means anyway */
  background: color-mix(in srgb, #ef4444 20%, transparent); color: #f87171;
}
html[data-theme="light"] .vhead span { color: #dc2626; }
.vlist a:hover { border-color: #ef4444; }
.vlist a:hover .vnum::before { color: #ef4444; }
.vlist a:hover .vplay { background: #ef4444; border-color: #ef4444; color: #fff; }

@media (max-width: 520px) { .ncard-links.is-pairs { grid-template-columns: 1fr; } }

/* the closing ask on a product page */
.ncta {
  margin: 30px 0 6px; padding: clamp(30px, 5vw, 44px) 30px; border-radius: 24px;
  text-align: center;
  border: 1px solid color-mix(in srgb, var(--accent-2) 32%, transparent);
  background:
    radial-gradient(110% 150% at 20% 0%, color-mix(in srgb, var(--accent-2) 22%, transparent), transparent 62%),
    radial-gradient(110% 150% at 100% 100%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 60%),
    var(--panel);
}
.ncta h3 { margin: 0; font-size: clamp(21px, 3vw, 27px); font-weight: 800; letter-spacing: -.01em; }
.ncta p { margin: 12px auto 0; max-width: 56ch; font-size: 14.5px; line-height: 1.7; color: var(--fg-2); }
.ncta .btn { margin-top: 20px; }
.ncta-note {
  margin-top: 16px !important; font-size: 12.5px !important; color: var(--fg-3) !important;
}


/* ── data provenance ──────────────────────────────────────────
   Attribution has to be findable without being loud. It sits at caption size
   in --fg-3; only the badge inside the panel carries colour. A page that
   shouts its sources reads as defensive — the point is that a researcher can
   answer "who says so?" in one glance and otherwise not notice.

   The trigger is a button, not a hover target: a `title` attribute is
   invisible on a phone and unreachable by keyboard. */
.jsrc {
  display: inline-flex; align-items: center; gap: 4px;
  margin: 4px 0 0; padding: 0; border: 0; background: none;
  font: inherit; font-size: 10px; line-height: 1.3; color: var(--fg-3);
  letter-spacing: 0; text-transform: none; cursor: pointer;
  max-width: 100%;
}
.jsrc:hover { color: var(--accent); }
.jsrc:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.jsrc-l { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The (i) is an inline <svg> — the circle is drawn, so there is no border to
   set, no font to be missing and no text-transform to turn it into a capital. */
.jsrc-i { flex: 0 0 auto; display: block; opacity: .55; }
.jsrc:hover .jsrc-i, .jsrc:focus-visible .jsrc-i { opacity: 1; }
.jsrc.is-icon { margin: 0 0 0 5px; vertical-align: middle; }

/* In the top layer, so the modal's scroll box, the table's horizontal scroll
   box and the label's own ellipsis clipping are all irrelevant. */
.jsrc-pop[popover], .jsrc-pop {
  margin: 0; padding: 14px 16px; border-radius: 14px;
  border: 1px solid var(--brd); background: var(--bg); color: var(--fg-2);
  box-shadow: 0 26px 60px -28px rgba(0, 0, 0, .85);
  max-width: min(340px, 88vw); font-size: 12.5px; line-height: 1.6;
  /* Centred in the viewport. `inset: auto` defeats the top layer's own
     centring and drops the panel at (0,0); `inset: 0` with auto margins is
     what actually centres it. Deliberately not anchored to the trigger:
     CSS anchor positioning is Chromium-only today, and a small panel that
     always appears in the same place is easier to follow than one that
     appears wherever the button happened to be. */
  inset: 0; margin: auto; height: fit-content; width: fit-content;
  /* The trigger lives inside a metric tile, whose caption is uppercase and
     letter-spaced. A popover is in the top layer but still inherits from its
     DOM parent, so without this the explanation renders in shouting capitals. */
  /* `.jsrc-pop[popover]` rather than `.jsrc-pop`: the trigger sits inside a
     metric tile and `.jtile span` (0,1,1) out-specifies a bare class (0,1,0),
     so the panel rendered in the tile's shouting uppercase caption style. */
  text-transform: none; letter-spacing: normal; text-align: left;
}
/* A fixed dark tint, not var(--bg): in the light theme the ground is #faf7fd,
   so a 55% wash of it over a light modal dimmed nothing at all and the panel
   read as though it had appeared by itself. */
.jsrc-pop::backdrop { background: rgba(8, 5, 18, .38); }
.jsrc-pop b { display: inline-block; margin-right: 8px; font-size: 13.5px; color: var(--fg); }
.jsrc-pop em { display: block; margin-top: 8px; font-style: normal; }
.jsrc-pop a { display: inline-block; margin-top: 10px; font-size: 12.5px; font-weight: 600; }
/* Hidden by default in the *author* origin, restating what the UA stylesheet
   already says. The UA rule is unreliable here: any author rule that sets
   `display` and happens to match — `.jtile span` did — silently defeats it,
   and a closed popover becomes an undismissable panel. `[popover]` lifts this
   to (0,2,0) so an ancestor's descendant selector cannot outrank it.
   Three separate rules, not one list: `:popover-open` is unknown to a browser
   without popover support, and one unknown selector invalidates a whole rule —
   which would take the `.is-open` fallback down with it. */
.jsrc-pop[popover] { display: none; }
/* the no-popover fallback positions this itself */
.jsrc-pop.is-open { display: block; }
.jsrc-pop[popover]:popover-open { display: block; }

.jtable th .jsrc { font-size: 9.5px; }
.jchart .jsrc { margin: -2px 0 10px; }

/* ── the per-record source line ─────────────────────────────── */
.jprov { margin: 22px 0 0; padding: 16px 18px 14px; border-radius: 14px;
  border: 1px solid var(--brd-2); background: color-mix(in srgb, var(--fg) 3%, transparent); }
.jprov h4 {
  margin: 0 0 10px; font-size: 10px; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase; color: var(--fg-3);
}
.jprov-line { display: flex; flex-wrap: wrap; gap: 7px; margin: 0; }
.jprov-chip {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 4px 8px 4px 10px; border-radius: 999px;
  border: 1px solid var(--brd); background: var(--panel);
  font-size: 12px; font-weight: 600; color: var(--fg-2);
}
/* One dot of colour per chip carries the only distinction that must not be
   missed: whose figure is this, and did we work it out ourselves? */
.jprov-chip::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%; margin-right: 4px;
}
.jprov-chip[data-cat="third_party"]::before { background: var(--accent-3); }
.jprov-chip[data-cat="primary"]::before     { background: var(--g3); }
.jprov-chip[data-cat="open_data"]::before   { background: #22c55e; }
.jprov-chip[data-cat="calculated"]::before  { background: var(--accent); }
.jprov-note { margin: 12px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--fg-3); }

@media (max-width: 640px) {
  /* On a phone a tooltip anchored to a narrow cell overflows the viewport.
     Pin it to the left edge of the modal instead of the element. */
  /* The popover is in the top layer and sizes itself; nothing to correct
     here any more. The table header keeps its (i) — it is 12px wide. */
  .jsrc-pop { max-width: 88vw; }
}

/* ── the methodology page ─────────────────────────────────── */
.dsrc-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
  gap: 14px; margin: 22px 0 0;
}
.dsrc {
  padding: 18px 20px; border-radius: 16px;
  border: 1px solid var(--brd); background: var(--panel);
}
.dsrc header { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.dsrc h3 { margin: 0; font-size: 15.5px; font-weight: 700; }
.dsrc p { margin: 10px 0 0; font-size: 13px; line-height: 1.65; color: var(--fg-2); }
.dsrc-link { margin-top: 12px !important; }
.dsrc-link a { font-size: 12.5px; font-weight: 600; }
/* The one visual cue that carries meaning: which of the three kinds this is. */
.dsrc[data-cat="third_party"] { border-left: 3px solid color-mix(in srgb, var(--accent-3) 60%, transparent); }
.dsrc[data-cat="primary"]     { border-left: 3px solid color-mix(in srgb, var(--g3) 70%, transparent); }
.dsrc[data-cat="open_data"]   { border-left: 3px solid color-mix(in srgb, #22c55e 60%, transparent); }
.dsrc[data-cat="calculated"]  { border-left: 3px solid color-mix(in srgb, var(--accent) 70%, transparent); }

.dsrc-note {
  padding: 18px 20px; border-radius: 14px;
  border: 1px solid var(--brd-2); background: color-mix(in srgb, var(--fg) 3%, transparent);
  font-size: 13.5px !important; line-height: 1.75 !important;
}
.dsrc-lists { list-style: none; padding: 0; margin: 18px 0 0; display: grid; gap: 8px; }
.dsrc-lists li { display: flex; align-items: baseline; gap: 10px; font-size: 14px; }
.dsrc-lists .n {
  font-family: "JetBrains Mono", monospace; font-size: 11.5px; color: var(--fg-3);
}

/* Publication activity: figures this site works out, sitting under the chart
   they are derived from so the arithmetic is checkable by eye. */
/* Each figure is a <div> wrapping its own <dt>/<dd> — valid inside a <dl>
   since HTML5, and necessary here: with dt and dd as bare siblings the grid
   fills them in document order, so a label lands in one column and its value
   in the next. */
.jact {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 12px 14px; margin: 14px 0 0; padding: 12px 0 0;
  border-top: 1px solid var(--brd-2);
}
.jact dt {
  font-size: 10px; font-weight: 600; letter-spacing: .07em;
  text-transform: uppercase; color: var(--fg-3);
}
.jact dd {
  margin: 3px 0 0; font-family: "JetBrains Mono", monospace;
  font-size: 15px; font-weight: 700; color: var(--fg);
}
.jact-src { margin: 10px 0 0; font-size: 10.5px; line-height: 1.5; color: var(--fg-3); }
