/* forn.al — shared stylesheet
 *
 * Brief: minimalist, functional, borderless. Text and tools only.
 *
 * Three rules, applied without exception:
 *
 * 1. NO BORDERS. No rules, boxes, cards, panels, shadows, or radii anywhere.
 *    Hierarchy comes from whitespace, type size, weight, and indentation.
 *    If a thing needs separating, give it air, not a line.
 *
 * 2. NO HUE. There is no accent colour, because there is nothing an accent
 *    colour could honestly say here. Every number on this site was counted by a
 *    command against a repository the reader cannot open, so a green "verified"
 *    mark would be a badge nobody can check — and a mark that lands on every
 *    number marks nothing at all. What gives a number its weight is the method
 *    and the date beside it, not a colour. Emphasis, where it is unavoidable,
 *    is --highlight: a shift in the paper, not a colour, at most once a page.
 *
 *    (The earlier version of this rule spent a desaturated green on "what a
 *    system claimed but wasn't true". That argument left with the failures it
 *    described; keeping the token and inverting it to mean "verified" would
 *    have been the conventional green check the original rule refused.)
 *
 * 3. MONO CARRIES THE PAGE. Anything a machine produced — a count, a status, a
 *    date, a label — is monospace. Serif appears only where a human is making
 *    an argument in sentences. Evidence dominates; prose is the exception.
 *
 * Zero JavaScript. Zero images. Zero external requests.
 */

:root {
  --paper:    #FCFCFC;
  --ink:      #111213;
  --muted:    #5E626A;
  --faint:    #9AA0A8;
  --highlight: #F0F0EE; /* used at most once per page, for the true value */
  --bar:      #24262A;  /* magnitude marks. ink, slightly recessive. */

  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
          "Liberation Mono", monospace;
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
           Georgia, serif;

  --measure: 42rem;
  --wide: 52rem;

  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.875rem;
  --s4: 1.5rem;
  --s5: 2.5rem;
  --s6: 4rem;
  --s7: 6rem;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper:    #0E0F10;
    --ink:      #E9E9E7;
    --muted:    #9CA1A8;
    --faint:    #656A71;
    --highlight: #1B1D1F;
    --bar:      #D2D2CE;
  }
}

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

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1.75;
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

.page {
  max-width: var(--wide);
  margin: 0 auto;
  padding: var(--s5) var(--s4) var(--s7);
}

/* ---- masthead ---------------------------------------------------------- */

.top {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--s6);
}

.who {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.top nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s3);
  font-size: 0.75rem;
  color: var(--muted);
  /* Flex items default to min-width:auto, which lets the nav refuse to shrink
     and push the whole page wider than the viewport on a phone. */
  min-width: 0;
}

/* ---- type -------------------------------------------------------------- */

h1 {
  font-family: var(--mono);
  font-size: clamp(1.5rem, 4.4vw, 2.375rem);
  font-weight: 600;
  line-height: 1.16;
  letter-spacing: -0.025em;
  margin: 0 0 var(--s4);
  max-width: 24ch;
  text-wrap: balance;
}

/* The address sits with the name rather than in a nav. The site is one page
   now, so a nav would be a row of one link pointing at the page you are on;
   the only thing left to navigate to is the inbox. */
h1 .h1sub {
  display: block;
  margin-top: var(--s3);
  font-size: 0.9375rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--muted);
}

h2 {
  font-family: var(--mono);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.4;
  margin: 0 0 var(--s3);
  letter-spacing: -0.01em;
}

/* Section labels. The only structural device on the page — they replace the
   rules and boxes a bordered design would use. */
.label {
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 var(--s4);
}

.label .when {
  float: right;
  letter-spacing: 0.08em;
  text-transform: none;
}

/* Prose is the exception, not the default. Serif signals "a person is making an
   argument here" as distinct from "a machine produced this". */
.prose {
  font-family: var(--serif);
  font-size: 1.0625rem;
  line-height: 1.62;
  color: var(--ink);
  max-width: var(--measure);
  margin: 0 0 var(--s3);
  font-variant-numeric: oldstyle-nums;
}

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

.lede {
  font-family: var(--serif);
  font-size: 1.1875rem;
  line-height: 1.55;
  max-width: var(--measure);
  margin: 0 0 var(--s4);
  font-variant-numeric: oldstyle-nums;
}

/* A remark under a chart or a count: the .label's faint, but sentence-cased
   and untracked, because it is an aside and not a heading. It carries the
   method for the number above it — which, on a page that prints counts and
   not commands, is the only place the method can live. Replaces the one
   inline style= the sheet used to need. */
.note {
  font-size: 0.75rem;
  color: var(--faint);
  margin: calc(-1 * var(--s2)) 0 var(--s5);
  max-width: var(--measure);
}

section { margin: 0 0 var(--s7); }

/* ---- links ------------------------------------------------------------- */

a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  text-decoration-color: var(--faint);
}
a:hover { text-decoration-thickness: 2px; text-decoration-color: currentColor; }
a:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* Navigation and the masthead are links by position, so they don't need to say so.
   Underlining every one of them makes the top of the page noisier than anything
   below it. */
.top a { text-decoration: none; }
.top a:hover { text-decoration: underline; text-decoration-thickness: 1px; }

/* The page you are on is the one set in full ink. Four items do not need a
   marker louder than the difference between grey and black, and with no
   borders and no hue available, a rule or a dot would be the only other way
   to say it. */
.top nav a[aria-current="page"] { color: var(--ink); }

/* ---- the tools --------------------------------------------------------- */

/* A borderless data list: label left, value right, aligned. */
.facts {
  font-size: 0.8125rem;
  line-height: 2.1;
  margin: 0 0 var(--s4);
  max-width: min(100%, 34rem);
}
.facts div {
  display: flex;
  gap: var(--s3);
  justify-content: space-between;
  align-items: baseline;
}
.facts dt, .facts .k { color: var(--muted); }
.facts dd, .facts .v { margin: 0; font-weight: 500; }

/* ---- the ticker --------------------------------------------------------
 * Three counts at display size, standing in for the 13px .facts list. Each
 * digit is its own inline-block and rolls up into place, staggered left to
 * right, settling on the plain number.
 *
 * The digits are the REAL digits, straight out of the HTML. That is the whole
 * design: the resting state is the counted value, so there is no state in
 * which a reader is shown a figure CSS invented. The obvious alternative --
 * @property plus counter(), spinning 0 to N -- has to rebuild the thousands
 * separators itself, and a synthesised number on this page would contradict
 * the one thing every other component here is arranged to say.
 *
 * prefers-reduced-motion needs no rule of its own: the reset near the end of
 * this sheet drops the animation, and what is left is translateY(0) at
 * opacity 1 -- the number, sitting still. A browser that ignores the
 * animation shorthand entirely lands in the same place, because fill-mode
 * only applies where the animation does. There is no in-between where the
 * figure is missing.
 *
 * Separators do not roll. On an odometer the commas are painted on.
 */
.tick {
  display: grid;
  /* auto-fit, not a breakpoint: at the clamp floor a nine-character figure is
     about 119px, so a phone takes two columns and a desktop three, and there
     is no width to keep in sync with the rest of the sheet. */
  grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
  gap: var(--s5) var(--s4);
  margin: 0 0 var(--s4);
  max-width: min(100%, 40rem);
}
.tick .v {
  display: block;
  font-size: clamp(1.375rem, 4.4vw, 2rem);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.03em;
}
.tick .k {
  display: block;
  font-size: 0.6875rem;
  color: var(--muted);
  line-height: 1.45;
  margin-top: var(--s2);
}

/* Text split into per-character elements is announced a character at a time
   by some screen readers. The digits leave the accessibility tree and the
   number is given once, whole. Both copies still ground against facts.json,
   because site_check reads text and throws attributes away. */
.tick .sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.tick .d > span {
  display: inline-block;
  animation: roll 520ms cubic-bezier(0.2, 0.75, 0.3, 1) both;
  animation-delay: calc(var(--base, 0ms) + var(--d, 0ms));
}
.tick .d > .sep { animation: none; }

/* Figure-level stagger first, then digit-level. Commas take a slot in the
   nth-child count, which groups the roll into thousands instead of flattening
   it into one even sweep. */
.tick > div:nth-child(2) { --base:  90ms; }
.tick > div:nth-child(3) { --base: 180ms; }

.tick .d > span:nth-child(2) { --d:  40ms; }
.tick .d > span:nth-child(3) { --d:  80ms; }
.tick .d > span:nth-child(4) { --d: 120ms; }
.tick .d > span:nth-child(5) { --d: 160ms; }
.tick .d > span:nth-child(6) { --d: 200ms; }
.tick .d > span:nth-child(7) { --d: 240ms; }
.tick .d > span:nth-child(8) { --d: 280ms; }
.tick .d > span:nth-child(9) { --d: 320ms; }

@keyframes roll {
  from { transform: translateY(0.5em); opacity: 0; }
  to   { transform: translateY(0);     opacity: 1; }
}

/* An HTTP trace. Real tool output, used where prose would be weaker. */
.trace {
  font-size: 0.8125rem;
  line-height: 1.95;
  color: var(--muted);
  margin: 0 0 var(--s4);
  white-space: pre-wrap;
  word-break: break-word;
}
.trace b { color: var(--ink); font-weight: 500; }

/* ---- magnitude bars ----------------------------------------------------
 * One series, so: no legend, no categorical palette, no hue. Marked up as a
 * real table, so the values read as data with the chart switched off, and
 * every bar is directly labelled — which is also why there is no hover
 * tooltip to add.
 */

.chart {
  width: 100%;
  max-width: min(100%, 40rem);
  border-collapse: collapse;
  font-size: 0.8125rem;
  margin: 0 0 var(--s4);
}
.chart caption {
  caption-side: top;
  text-align: left;
  font-size: 0.6875rem;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--faint);
  padding-bottom: var(--s3);
}
.chart td { padding: 2px 0; vertical-align: middle; }
.chart .lbl { color: var(--muted); white-space: nowrap; padding-right: var(--s3); }
.chart .track { width: 100%; }
.chart .n {
  text-align: right;
  padding-left: var(--s3);
  white-space: nowrap;
  font-weight: 500;
}
/* Square ends. A 3px radius here was the sheet's only violation of rule 1,
   and it shipped. */
.chart .bar {
  height: 10px;
  background: var(--bar);
}

/* The dedupe comparison carries the headline, so its bars are heavier and it
   drops the label column entirely. */
.funnel .bar { height: 22px; }
.funnel .lbl { color: var(--ink); }
.funnel .quiet .bar { background: var(--faint); }

/* roles */
.role { margin: 0 0 var(--s4); max-width: var(--measure); }
.role .rh {
  display: flex;
  gap: var(--s3);
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.875rem;
  font-weight: 600;
}
/* --muted, not --faint: at 12px --faint is 2.57:1 on light paper and 3.52:1 on
   dark, under WCAG AA's 4.5:1, and this is the one figure each entry carries. */
.role .yr { color: var(--muted); font-weight: 400; font-size: 0.75rem; }
.role p {
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--muted);
  margin: var(--s1) 0 0;
  font-variant-numeric: oldstyle-nums;
}

/* Where the thing actually is. Mono, against the serif description above it,
   because a URL is a machine's name for a place and not part of the argument. */
.role .links {
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.9;
  color: var(--muted);
  margin: var(--s2) 0 0;
}

/* footer. --muted for the same reason as .role .yr: it carries the work history. */
.foot {
  margin-top: var(--s7);
  font-size: 0.75rem;
  color: var(--muted);
  line-height: 2;
}
.foot .stamp { display: block; margin-top: var(--s3); }

@media (max-width: 40rem) {
  body { font-size: 14px; }
  .page { padding: var(--s4) var(--s3) var(--s6); }

  /* Label/value pairs stack rather than stretching to opposite edges — at this
     width a right-aligned value reads as unrelated to its label. */
  .facts div { display: block; line-height: 1.5; margin-bottom: var(--s3); }
  .facts .k, .facts dt { display: block; font-size: 0.75rem; }
  .facts .v, .facts dd { display: block; margin-left: 0; font-size: 0.875rem; }

  /* The date in a section label sits under the label instead of beside it. */
  .label .when { float: none; display: block; margin-top: var(--s1); }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* ---- résumé ------------------------------------------------------------
 * The one page with bullets. They get whitespace and a hanging indent rather
 * than a marker glyph, for the same reason nothing else here has a border:
 * the alignment already says "these are a list".
 */

main ul { list-style: none; padding-left: 0; margin: var(--s1) 0 0; }
main li {
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--muted);
  font-variant-numeric: oldstyle-nums;
  max-width: var(--measure);
  padding-left: 1.1em;
  text-indent: -1.1em;
  margin-bottom: var(--s2);
}
main li::before { content: "· "; color: var(--faint); }

/* A skills line: a category and the terms under it. NOT .facts -- that one
   pushes a label and a value to opposite edges, which is right for
   "migrations 207" and falls apart on a category name beside forty terms,
   wrapping both columns into ragged stacks. Here the category sits above its
   terms and the indent does the joining, so the terms keep the full measure. */
.skill { margin: 0 0 var(--s3); max-width: var(--measure); font-size: 0.8125rem; }
.skill .k { display: block; color: var(--muted); }
.skill .v { display: block; line-height: 1.7; }

/* ---- print -------------------------------------------------------------
 * prefers-color-scheme: dark still applies while printing, so without this a
 * dark-mode reader prints the résumé as light-on-dark — or, once the printer
 * drops the background, near-white text on white paper. The résumé is the one
 * page on this site somebody will actually put on paper.
 */
@media print {
  :root {
    --paper: #fff; --ink: #000; --muted: #333; --faint: #555;
    --highlight: #eee; --bar: #000;
  }
  body { font-size: 11pt; }
  .page { max-width: none; padding: 0; }
  .top nav, .foot { display: none; }
  a { text-decoration: none; }
  /* On paper a link is only useful if it says where it goes. */
  main a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; }
  .role, main li { break-inside: avoid; }
  h1, h2, .label { break-after: avoid; }
}
