:root {
  --bg: #fbfaf8;
  --fg: #1c1b19;
  --muted: #6b6864;
  --rule: #e3e0da;
  --accent: #9a5b2c;
  --card: #ffffff;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16151a;
    --fg: #e8e6e1;
    --muted: #9b978f;
    --rule: #2e2c33;
    --accent: #d9975b;
    --card: #1e1d23;
  }
}
* { box-sizing: border-box; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
}
/* Translucent only where the blur is actually supported; otherwise the solid
   background above stands, so text never sits on top of scrolling content. */
@supports (backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px)) {
  .site-header {
    background: color-mix(in srgb, var(--bg) 80%, transparent);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }
}
.site-header-inner {
  display: flex; flex-wrap: wrap; gap: 1rem; align-items: baseline;
  justify-content: space-between;
  max-width: 46rem; margin: 0 auto; padding: .85rem 1.25rem;
}
.brand { font-weight: 650; font-size: 1.05rem; color: var(--fg); text-decoration: none; }
.site-header-inner nav { display: flex; gap: 1.1rem; }
.site-header-inner nav a { color: var(--muted); text-decoration: none; font-size: .92rem; }
.site-header-inner nav a:hover { color: var(--accent); }
main { max-width: 46rem; margin: 0 auto; padding: 2rem 1.25rem 3rem; }
.hero h1 { font-size: 2.1rem; margin: 0 0 .3rem; letter-spacing: -.02em; }
.tagline { color: var(--muted); margin: 0 0 2rem; }
.prose h1 { font-size: 1.9rem; letter-spacing: -.02em; margin-top: 0; }
.prose h2 {
  font-size: 1.05rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--muted); margin: 2.4rem 0 .8rem;
  border-bottom: 1px solid var(--rule); padding-bottom: .35rem;
}
.prose a { color: var(--accent); }
.site-footer {
  max-width: 46rem; margin: 0 auto; padding: 1.25rem;
  border-top: 1px solid var(--rule); color: var(--muted); font-size: .85rem;
}

/* Sensor page */
.controls { display: flex; gap: .5rem; margin: 1.25rem 0 .75rem; flex-wrap: wrap; }
.controls button {
  font: inherit; font-size: .85rem; padding: .3rem .75rem; cursor: pointer;
  background: transparent; color: var(--muted);
  border: 1px solid var(--rule); border-radius: 999px;
}
.controls button[aria-pressed="true"] { color: var(--bg); background: var(--accent); border-color: var(--accent); }
.status { color: var(--muted); font-size: .85rem; }
.status.error { color: #c0392b; }

/* CV page */
.prose h3 {
  font-size: .95rem; font-weight: 650; color: var(--fg);
  margin: 1.8rem 0 .6rem;
}
.prose h2 + p, .prose h3 + p { margin-top: 0; }
/* Publication and presentation entries: hanging indent, tighter leading. */
.prose h2#publications ~ p,
.prose h2#presentations ~ p {
  font-size: .92rem; line-height: 1.55;
  margin: 0 0 .9rem; padding-left: 1.4rem; text-indent: -1.4rem;
}
.prose h2#technical-skills ~ p { margin: 0 0 .7rem; }
.prose ul { padding-left: 1.1rem; }
.prose li { margin-bottom: .3rem; }
.tag {
  display: inline-block; font-size: .68rem; text-transform: uppercase;
  letter-spacing: .07em; vertical-align: middle;
  padding: .1rem .45rem; margin-left: .35rem;
  border: 1px solid var(--rule); border-radius: 999px; color: var(--muted);
}

/* Header dropdown */
html { scroll-behavior: smooth; }
.prose h2, .prose h3 { scroll-margin-top: 5rem; }

.nav-dropdown { position: relative; display: inline-block; }
.nav-dropdown > a { display: inline-flex; align-items: center; gap: .2rem; }
.nav-dropdown .caret { font-size: .65em; transition: transform .15s ease; }
.nav-dropdown-menu {
  position: absolute; right: 0; top: 100%; z-index: 20;
  margin: .4rem 0 0; padding: .35rem;
  min-width: 11rem; list-style: none;
  background: var(--card);
  border: 1px solid var(--rule); border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .12);
  opacity: 0; visibility: hidden; transform: translateY(-.25rem);
  transition: opacity .14s ease, transform .14s ease, visibility .14s;
}
/* :focus-within keeps this usable from the keyboard without any JS. */
.nav-dropdown:hover .nav-dropdown-menu,
.nav-dropdown:focus-within .nav-dropdown-menu {
  opacity: 1; visibility: visible; transform: translateY(0);
}
.nav-dropdown:hover .caret,
.nav-dropdown:focus-within .caret { transform: rotate(180deg); }
.nav-dropdown-menu li { margin: 0; }
.nav-dropdown-menu a {
  display: block; padding: .35rem .6rem; border-radius: 6px;
  font-size: .88rem; color: var(--fg); text-decoration: none; white-space: nowrap;
}
.nav-dropdown-menu a:hover, .nav-dropdown-menu a:focus-visible {
  background: var(--rule); color: var(--accent); outline: none;
}
/* A hover-opened menu can sit off-screen on a narrow phone; let it wrap
   under the header instead. */
@media (max-width: 30rem) {
  .nav-dropdown-menu { right: auto; left: 0; min-width: 9rem; }
}

/* SOM composite tool */
.som-form { margin: 1.5rem 0 .75rem; }
.som-form .som-field + .som-field { margin-top: .7rem; }
.som-window-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
/* Scoped under .som-field so this beats the full-width `.som-field input`
   rule below on specificity rather than on source order -- otherwise the two
   year boxes stretch and stack. */
.som-field .som-window-row input { width: 6rem; font: inherit; padding: .4rem .5rem;
  color: var(--fg); background: var(--card);
  border: 1px solid var(--rule); border-radius: 8px; }
.som-field .som-window-row input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.som-window-row > span { color: var(--muted); }
#base-reset { font: inherit; font-size: .8rem; padding: .25rem .7rem; cursor: pointer;
  background: transparent; color: var(--muted);
  border: 1px solid var(--rule); border-radius: 999px; }
#base-reset:hover { color: var(--accent); border-color: var(--accent); }
.som-field { display: block; }
.som-field span {
  display: block; font-size: .72rem; text-transform: uppercase;
  letter-spacing: .09em; color: var(--muted); margin-bottom: .3rem;
}
.som-field input {
  font: inherit; width: 100%; padding: .5rem .7rem;
  color: var(--fg); background: var(--card);
  border: 1px solid var(--rule); border-radius: 8px;
}
.som-field input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.som-row { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: center; }
.som-row .controls { margin: .9rem 0 0; }

.som-grids { display: grid; grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr)); gap: 1rem; margin: 1rem 0; }
.som-fig { margin: 0; }
.som-fig figcaption {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted); margin-bottom: .4rem;
}
.som-grid {
  /* JS overrides this from the data, but stating it here means the lattice
     survives even if the script never runs. */
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 2px;
  background: var(--rule); border: 1px solid var(--rule); border-radius: 8px;
  overflow: hidden;
}
.som-cell {
  aspect-ratio: 5 / 4;
  display: flex; align-items: center; justify-content: center;
  font-size: .85rem; font-weight: 650; font-variant-numeric: tabular-nums;
  /* Cell fills come from the same colormaps as the figure and are light in
     both themes, so the label stays dark with a light halo rather than
     following --fg. */
  color: #111;
  text-shadow: 0 0 2px #fff, 0 0 2px #fff, 0 0 2px #fff;
  cursor: default;
}
.som-scale { margin-top: .4rem; }
.som-scale-bar { display: flex; height: .5rem; border-radius: 3px; overflow: hidden; }
.som-scale-bar span { flex: 1; }
.som-scale-ticks {
  font-size: .72rem; color: var(--muted);
  font-variant-numeric: tabular-nums; margin-top: .2rem;
}

.som-downloads {
  display: flex; flex-wrap: wrap; gap: .6rem; align-items: baseline;
  margin: 1.5rem 0 0; font-size: .85rem; color: var(--muted);
}
.som-downloads a {
  color: var(--accent); text-decoration: none;
  border: 1px solid var(--rule); border-radius: 999px; padding: .15rem .6rem;
}
.som-downloads a:hover { border-color: var(--accent); }
.som-dl-note { font-size: .8rem; }

/* SOM node patterns */
.som-patterns {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: .35rem;
  margin: .75rem 0 0;
}
.som-patterns a { display: block; line-height: 0; border-radius: 4px; overflow: hidden; }
.som-patterns a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.som-patterns img { width: 100%; height: auto; display: block; background: #fff; }
/* The panels are drawn on white, so on a dark background they need a frame to
   avoid floating; on light it just reads as a hairline. */
.som-patterns a { border: 1px solid var(--rule); }
.som-cbar { width: 100%; height: auto; margin-top: .5rem; background: #fff;
  border: 1px solid var(--rule); border-radius: 4px; }

/* Figure panel picker */
.som-panels {
  margin: 1.5rem 0 0; padding: .75rem 1rem 1rem;
  border: 1px solid var(--rule); border-radius: 10px;
  display: flex; flex-wrap: wrap; gap: .35rem 1.25rem;
}
.som-panels legend {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted); padding: 0 .35rem;
}
.som-panels label {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .88rem; cursor: pointer;
}
.som-panels input { accent-color: var(--accent); cursor: pointer; }
/* Set B stays visible when it does not apply, so the row of options does not
   reflow as the second years field is filled in. */
.som-panels-sep {
  flex-basis: 100%; height: 0; margin: .1rem 0;
  border-top: 1px dashed var(--rule);
}
.som-panels label.is-off { opacity: .45; cursor: default; }
.som-panels label.is-off input { cursor: default; }
.som-downloads.is-empty a {
  opacity: .45; pointer-events: none; border-style: dashed;
}

/* Citation beside the downloads: the paper is what should be cited, not this
   page or the code repository. */
.som-cite {
  margin: .9rem 0 0; font-size: .8rem; line-height: 1.5; color: var(--muted);
  border-left: 2px solid var(--rule); padding-left: .75rem;
}
.som-cite > span {
  display: block; text-transform: uppercase; letter-spacing: .09em;
  font-size: .68rem; margin-bottom: .25rem;
}
.som-cite ul { list-style: none; margin: 0; padding: 0; }
.som-cite li + li { margin-top: .5rem; }
.som-cite em {
  display: block; font-style: normal; color: var(--fg);
  font-size: .72rem; text-transform: uppercase; letter-spacing: .07em;
}
.som-cite a { color: var(--accent); }

/* BibTeX copy */
.som-bib { display: flex; align-items: baseline; gap: .9rem; flex-wrap: wrap; margin-top: .6rem; }
#bib-copy {
  font: inherit; font-size: .8rem; padding: .25rem .8rem; cursor: pointer;
  background: transparent; color: var(--accent);
  border: 1px solid var(--rule); border-radius: 999px;
}
#bib-copy:hover { border-color: var(--accent); }
.som-bib details { font-size: .8rem; color: var(--muted); }
.som-bib summary { cursor: pointer; }
.som-bib pre {
  margin: .5rem 0 0; padding: .75rem .9rem;
  background: var(--card); border: 1px solid var(--rule); border-radius: 8px;
  font-size: .74rem; line-height: 1.45; overflow-x: auto;
  white-space: pre; color: var(--fg);
}
/* The details sits inside a flex row; let its contents span the full width. */
.som-bib details[open] { flex-basis: 100%; }

/* Reuse terms, under the citation */
.som-terms {
  margin: .5rem 0 0; font-size: .78rem; line-height: 1.5; color: var(--muted);
  border-left: 2px solid var(--rule); padding-left: .75rem;
}
.som-terms a { color: var(--accent); }

/* --- Lightning outlook --------------------------------------------------- */
/* The non-active states are the common case: the outlook runs four months a
   year, so "why isn't there one" is what most visitors will read. It gets the
   same visual weight as the table rather than being a footnote. */
.ol-state {
  padding: 1rem 1.1rem;
  margin: 1.25rem 0;
  border: 1px solid var(--rule);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
  background: var(--card);
  line-height: 1.55;
}
.ol-state.ol-warn { border-left-color: #b45309; }

.ol-table-wrap { overflow-x: auto; margin: 0.75rem 0; }
.ol-table { width: 100%; border-collapse: collapse; font-size: 0.94rem; }
.ol-table th, .ol-table td {
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--rule);
  text-align: left;
}
.ol-table th { font-weight: 600; white-space: nowrap; }
.ol-table td.num, .ol-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.ol-table tbody tr:hover { background: var(--card); }

/* Absolute magnitude and anomaly answer different questions -- is this a lot,
   and is this unusual -- and neither substitutes for the other, so the ratio
   is emphasised without hiding the counts it sits beside. */
.ol-ratio { font-weight: 600; }
.ol-high { color: #b91c1c; }
.ol-mid  { color: #b45309; }
.ol-low  { color: #1d4ed8; }
.ol-quiet { color: var(--muted); font-size: 0.88em; }
.ol-note { color: var(--muted); font-size: 0.86rem; line-height: 1.5; margin-top: 0.6rem; }

/* --- Outlook map --------------------------------------------------------- */
.ol-map-wrap { position: relative; margin: 1rem 0 .5rem; }
#ol-map { width: 100%; height: auto; display: block; }
.ol-psa {
  stroke: var(--bg); stroke-width: 1.4; stroke-linejoin: round;
  transition: opacity .12s;
}
.ol-psa:hover { opacity: .78; cursor: default; }

/* Follows the cursor, so it must never sit under it and re-trigger leave. */
.ol-tip {
  position: absolute; pointer-events: none; z-index: 5;
  background: var(--card); border: 1px solid var(--rule); border-radius: 6px;
  padding: .45rem .6rem; font-size: .82rem; line-height: 1.35;
  box-shadow: 0 2px 10px rgba(0,0,0,.14); max-width: 16rem;
}
.ol-tip strong { display: block; }
.ol-tip span { display: block; color: var(--muted); }
.ol-tip em { font-style: normal; }

.ol-legend {
  display: flex; flex-wrap: wrap; gap: .4rem 1rem;
  font-size: .78rem; color: var(--muted); margin-bottom: 1rem;
}
.ol-key { display: inline-flex; align-items: center; gap: .35rem; }
.ol-key i {
  width: .85rem; height: .85rem; border-radius: 3px;
  border: 1px solid var(--rule); display: inline-block;
}

/* --- Off-season demonstration -------------------------------------------- */
/* The demo renders through the same components as a live outlook, so the
   labelling carries the whole burden of distinguishing them. It is loud on
   purpose, and the striped rail repeats down the side of the rendered block
   so the marking is still visible after scrolling past the banner. */
.ol-demo-banner {
  margin: 1.25rem 0 .25rem;
  padding: .7rem .9rem;
  border: 1px solid #b45309;
  border-left: 4px solid #b45309;
  border-radius: 6px;
  background: #fff7ed;
  color: #7c2d12;
  font-size: .88rem;
  line-height: 1.5;
}
.ol-is-demo {
  border-left: 4px repeating-linear-gradient(#b45309 0 6px, transparent 6px 12px);
  border-left: 4px solid #f0c9a4;
  padding-left: .9rem;
}
.ol-is-demo #ol-issued { color: #b45309; font-weight: 600; }

/* Block buttons lead with the valid date; the period length is secondary. */
.controls button small {
  display: block; font-size: .74em; opacity: .75; margin-top: .05rem;
}

/* --- Outlook context ----------------------------------------------------- */
/* Reliability sits with the period it describes rather than in a footnote, so
   a reader choosing the 9-day view sees the 9-day number. */
.ol-skill {
  font-size: .88rem; line-height: 1.5; margin: .6rem 0 .2rem;
  padding: .55rem .75rem; border-left: 3px solid var(--accent);
  background: var(--card); border-radius: 0 5px 5px 0;
}
.ol-skill a { color: var(--accent); }

.ol-details { margin: 1.25rem 0; border-top: 1px solid var(--rule); }
.ol-details summary {
  cursor: pointer; padding: .7rem 0; font-size: .9rem; font-weight: 600;
}
.ol-details-body { font-size: .9rem; line-height: 1.6; padding-bottom: .5rem; }
.ol-details-body h3 {
  font-size: .8rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); margin: 1.2rem 0 .4rem;
}
.ol-details-body ul { padding-left: 1.1rem; }
.ol-details-body li { margin-bottom: .45rem; }
.ol-skill-table { border-collapse: collapse; font-size: .86rem; margin: .5rem 0; }
.ol-skill-table th, .ol-skill-table td {
  padding: .3rem .8rem .3rem 0; text-align: left;
  border-bottom: 1px solid var(--rule);
}
.ol-skill-table th { font-weight: 600; color: var(--muted); }
.ol-cite { font-size: .85rem; color: var(--muted); line-height: 1.5; }

/* Per-area reliability. Muted by design: it qualifies the forecast rather
   than competing with it, but "limited" is marked so it is not missed. */
.ol-tip-skill { margin-top: .25rem; font-size: .92em; }
.ol-rel { font-size: .86em; color: var(--muted); white-space: nowrap; }
.ol-rel-good { color: #15803d; }
.ol-rel-limited { color: #b45309; }
.ol-rel-none { font-style: italic; opacity: .7; }
